// Copyright Pacificao. All Rights Reserved.

#include "AgrarianGameState.h"
#include "AgrarianPerformanceStats.h"
#include "Net/UnrealNetwork.h"
#include "ProfilingDebugging/CpuProfilerTrace.h"

namespace
{
int32 NormalizeCalendarDay(int32 DayOfYear, int32 DaysPerYear)
{
	const int32 SafeDaysPerYear = FMath::Max(1, DaysPerYear);
	const int32 ZeroBasedDay = ((DayOfYear - 1) % SafeDaysPerYear + SafeDaysPerYear) % SafeDaysPerYear;
	return ZeroBasedDay + 1;
}

bool IsDayInRange(int32 DayOfYear, int32 StartDay, int32 EndDay, int32 DaysPerYear)
{
	const int32 Day = NormalizeCalendarDay(DayOfYear, DaysPerYear);
	const int32 Start = NormalizeCalendarDay(StartDay, DaysPerYear);
	const int32 End = NormalizeCalendarDay(EndDay, DaysPerYear);

	if (Start <= End)
	{
		return Day >= Start && Day <= End;
	}

	return Day >= Start || Day <= End;
}

float GetWrappedHourDelta(float FromHour, float ToHour)
{
	float Delta = FMath::Fmod(ToHour - FromHour, 24.0f);
	if (Delta < 0.0f)
	{
		Delta += 24.0f;
	}
	return Delta;
}
}

AAgrarianGameState::AAgrarianGameState()
{
	PrimaryActorTick.bCanEverTick = true;
	bReplicates = true;
	ActiveGrowingSeason.TileId = ActiveSolarTileId;
	ActiveGrowingSeason.GrowingZoneLabel = TEXT("USDA 10a");
	ActiveGrowingSeason.GrowingSeasonStartDay = 46;
	ActiveGrowingSeason.GrowingSeasonEndDay = 350;
	ActiveGrowingSeason.FrostFreeDays = 305;
	ActiveGrowingSeason.MinAverageGrowingTempC = 7.0f;
	ActiveGrowingSeason.CropSafetyBufferDays = 14;
	ActiveGrowingSeason.ClimateProfile = TEXT("coastal_mediterranean_mild");
	ActiveWeatherInputs.TileId = ActiveSolarTileId;
	ActiveWeatherInputs.Latitude = ActiveTileLatitude;
	ActiveWeatherInputs.Longitude = ActiveTileLongitude;
	ActiveWeatherDebug.TileId = ActiveSolarTileId;
	ActiveWeatherDebug.Latitude = ActiveTileLatitude;
	ActiveWeatherDebug.Longitude = ActiveTileLongitude;
	ActiveWeatherDebug.AppliedWeather = Weather;
}

void AAgrarianGameState::BeginPlay()
{
	Super::BeginPlay();

	if (HasAuthority())
	{
		UpdateSolarTimes();
	}
}

void AAgrarianGameState::Tick(float DeltaSeconds)
{
	SCOPE_CYCLE_COUNTER(STAT_AgrarianGameStateTick);
	TRACE_CPUPROFILER_EVENT_SCOPE(AgrarianGameStateTick);

	Super::Tick(DeltaSeconds);

	if (!HasAuthority())
	{
		return;
	}

	WorldHours += (DeltaSeconds / 60.0f) * GameHoursPerRealMinute;
	while (WorldHours >= 24.0f)
	{
		WorldHours -= 24.0f;
		const int32 PreviousDayOfYear = ActiveDayOfYear;
		ActiveDayOfYear = (ActiveDayOfYear % FMath::Max(1, DaysPerAgrarianYear)) + 1;
		if (ActiveDayOfYear < PreviousDayOfYear)
		{
			++ActiveYear;
		}
		UpdateSolarTimes();
	}

	UpdateAmbientTemperature();
}

void AAgrarianGameState::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& OutLifetimeProps) const
{
	Super::GetLifetimeReplicatedProps(OutLifetimeProps);
	DOREPLIFETIME(AAgrarianGameState, WorldHours);
	DOREPLIFETIME(AAgrarianGameState, Weather);
	DOREPLIFETIME(AAgrarianGameState, AmbientTemperatureC);
	DOREPLIFETIME(AAgrarianGameState, RegionalDailyLowTemperatureC);
	DOREPLIFETIME(AAgrarianGameState, RegionalDailyHighTemperatureC);
	DOREPLIFETIME(AAgrarianGameState, RegionalObservedTemperatureC);
	DOREPLIFETIME(AAgrarianGameState, ObservedTemperatureBlend);
	DOREPLIFETIME(AAgrarianGameState, bHasRegionalObservedTemperature);
	DOREPLIFETIME(AAgrarianGameState, RegionalWeatherSource);
	DOREPLIFETIME(AAgrarianGameState, ActiveWeatherInputs);
	DOREPLIFETIME(AAgrarianGameState, ActiveWeatherDebug);
	DOREPLIFETIME(AAgrarianGameState, DaysPerAgrarianYear);
	DOREPLIFETIME(AAgrarianGameState, ActiveSolarTileId);
	DOREPLIFETIME(AAgrarianGameState, ActiveTileLatitude);
	DOREPLIFETIME(AAgrarianGameState, ActiveTileLongitude);
	DOREPLIFETIME(AAgrarianGameState, ActiveTileTimeZoneId);
	DOREPLIFETIME(AAgrarianGameState, ActiveTileUtcOffsetHours);
	DOREPLIFETIME(AAgrarianGameState, ActiveDayOfYear);
	DOREPLIFETIME(AAgrarianGameState, ActiveYear);
	DOREPLIFETIME(AAgrarianGameState, ActiveGrowingSeason);
	DOREPLIFETIME(AAgrarianGameState, bHasActiveTileSolarData);
	DOREPLIFETIME(AAgrarianGameState, SunriseHourLocal);
	DOREPLIFETIME(AAgrarianGameState, SunsetHourLocal);
	DOREPLIFETIME(AAgrarianGameState, SolarNoonHourLocal);
	DOREPLIFETIME(AAgrarianGameState, DayLengthHours);
}

bool AAgrarianGameState::IsNight() const
{
	if (bHasActiveTileSolarData)
	{
		return WorldHours < SunriseHourLocal || WorldHours > SunsetHourLocal;
	}

	return WorldHours < 6.0f || WorldHours > 20.0f;
}

void AAgrarianGameState::SetWeather(EAgrarianWeatherType NewWeather)
{
	if (HasAuthority())
	{
		Weather = NewWeather;
		ActiveWeatherDebug.AppliedWeather = NewWeather;
		UpdateAmbientTemperature();
		OnRep_Weather();
	}
}

void AAgrarianGameState::SetRegionalTemperatureProfile(float DailyLowTemperatureC, float DailyHighTemperatureC)
{
	if (!HasAuthority())
	{
		return;
	}

	RegionalDailyLowTemperatureC = FMath::Clamp(FMath::Min(DailyLowTemperatureC, DailyHighTemperatureC), -80.0f, 70.0f);
	RegionalDailyHighTemperatureC = FMath::Clamp(FMath::Max(DailyLowTemperatureC, DailyHighTemperatureC), -80.0f, 70.0f);
	UpdateAmbientTemperature();
}

void AAgrarianGameState::SetRegionalObservedTemperature(float ObservedTemperatureC, float BlendWeight, const FString& WeatherSource)
{
	if (!HasAuthority())
	{
		return;
	}

	RegionalObservedTemperatureC = FMath::Clamp(ObservedTemperatureC, -80.0f, 70.0f);
	ObservedTemperatureBlend = FMath::Clamp(BlendWeight, 0.0f, 1.0f);
	bHasRegionalObservedTemperature = ObservedTemperatureBlend > 0.0f;
	RegionalWeatherSource = WeatherSource.IsEmpty() ? TEXT("server_weather_adapter") : WeatherSource;
	UpdateAmbientTemperature();
}

void AAgrarianGameState::ApplyMappedWeatherInputs(const FAgrarianMappedWeatherInputs& MappedInputs)
{
	if (!HasAuthority())
	{
		return;
	}

	ActiveWeatherInputs = MappedInputs;
	ActiveWeatherInputs.TemperatureC = FMath::Clamp(ActiveWeatherInputs.TemperatureC, -80.0f, 70.0f);
	ActiveWeatherInputs.DailyLowTemperatureC = FMath::Clamp(ActiveWeatherInputs.DailyLowTemperatureC, -80.0f, 70.0f);
	ActiveWeatherInputs.DailyHighTemperatureC = FMath::Clamp(ActiveWeatherInputs.DailyHighTemperatureC, -80.0f, 70.0f);
	ActiveWeatherInputs.PrecipitationMm = FMath::Max(0.0f, ActiveWeatherInputs.PrecipitationMm);
	ActiveWeatherInputs.WindSpeedKmh = FMath::Max(0.0f, ActiveWeatherInputs.WindSpeedKmh);
	ActiveWeatherInputs.CloudCoverPercent = FMath::Clamp(ActiveWeatherInputs.CloudCoverPercent, 0.0f, 100.0f);
	ActiveWeatherInputs.RelativeHumidityPercent = FMath::Clamp(ActiveWeatherInputs.RelativeHumidityPercent, 0.0f, 100.0f);
	ActiveWeatherInputs.PressureMslHpa = FMath::Clamp(ActiveWeatherInputs.PressureMslHpa, 800.0f, 1100.0f);
	ActiveWeatherInputs.VisibilityMeters = FMath::Max(0.0f, ActiveWeatherInputs.VisibilityMeters);
	ActiveWeatherInputs.bHasProviderData = true;

	ActiveWeatherDebug.TileId = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.TileId : ActiveSolarTileId;
	ActiveWeatherDebug.Latitude = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.Latitude : ActiveTileLatitude;
	ActiveWeatherDebug.Longitude = ActiveWeatherInputs.TileId != NAME_None ? ActiveWeatherInputs.Longitude : ActiveTileLongitude;
	ActiveWeatherDebug.Provider = ActiveWeatherInputs.Provider;
	ActiveWeatherDebug.ProviderTimestamp = ActiveWeatherInputs.ProviderTimestamp;
	ActiveWeatherDebug.AppliedWeather = ActiveWeatherInputs.MappedWeather;
	ActiveWeatherDebug.ProviderWeatherCode = ActiveWeatherInputs.ProviderWeatherCode;
	ActiveWeatherDebug.TemperatureC = ActiveWeatherInputs.TemperatureC;
	ActiveWeatherDebug.PrecipitationMm = ActiveWeatherInputs.PrecipitationMm;
	ActiveWeatherDebug.WindSpeedKmh = ActiveWeatherInputs.WindSpeedKmh;
	ActiveWeatherDebug.CloudCoverPercent = ActiveWeatherInputs.CloudCoverPercent;
	ActiveWeatherDebug.RelativeHumidityPercent = ActiveWeatherInputs.RelativeHumidityPercent;
	ActiveWeatherDebug.PressureMslHpa = ActiveWeatherInputs.PressureMslHpa;
	ActiveWeatherDebug.VisibilityMeters = ActiveWeatherInputs.VisibilityMeters;
	ActiveWeatherDebug.bHasProviderData = ActiveWeatherInputs.bHasProviderData;

	SetRegionalTemperatureProfile(ActiveWeatherInputs.DailyLowTemperatureC, ActiveWeatherInputs.DailyHighTemperatureC);
	SetRegionalObservedTemperature(
		ActiveWeatherInputs.TemperatureC,
		1.0f,
		FString::Printf(TEXT("%s:%s"), *ActiveWeatherInputs.Provider, *ActiveWeatherInputs.ProviderTimestamp));
	SetWeather(ActiveWeatherInputs.MappedWeather);
}

FAgrarianWeatherDebugSnapshot AAgrarianGameState::GetWeatherDebugSnapshot() const
{
	return ActiveWeatherDebug;
}

float AAgrarianGameState::GetClearSkyTemperatureForHour(float HourOfDay) const
{
	const float LowTemperature = FMath::Min(RegionalDailyLowTemperatureC, RegionalDailyHighTemperatureC);
	const float HighTemperature = FMath::Max(RegionalDailyLowTemperatureC, RegionalDailyHighTemperatureC);
	const float LowHour = bHasActiveTileSolarData ? SunriseHourLocal : 6.0f;
	const float HighHour = bHasActiveTileSolarData ? FMath::Fmod(SolarNoonHourLocal + 3.0f, 24.0f) : 14.0f;
	const float NormalizedHour = FMath::Fmod(HourOfDay, 24.0f) < 0.0f
		? FMath::Fmod(HourOfDay, 24.0f) + 24.0f
		: FMath::Fmod(HourOfDay, 24.0f);

	const float RisingDuration = FMath::Max(0.1f, GetWrappedHourDelta(LowHour, HighHour));
	const float HoursSinceLow = GetWrappedHourDelta(LowHour, NormalizedHour);
	if (HoursSinceLow <= RisingDuration)
	{
		const float Alpha = 0.5f - (0.5f * FMath::Cos(PI * (HoursSinceLow / RisingDuration)));
		return FMath::Lerp(LowTemperature, HighTemperature, Alpha);
	}

	const float CoolingDuration = FMath::Max(0.1f, 24.0f - RisingDuration);
	const float HoursSinceHigh = GetWrappedHourDelta(HighHour, NormalizedHour);
	const float Alpha = 0.5f - (0.5f * FMath::Cos(PI * (HoursSinceHigh / CoolingDuration)));
	return FMath::Lerp(HighTemperature, LowTemperature, Alpha);
}

bool AAgrarianGameState::ConfigureActiveSolarTile(FName TileId, float Latitude, float Longitude, const FString& TimeZoneId, float UtcOffsetHours)
{
	if (!HasAuthority() || TileId == NAME_None)
	{
		return false;
	}

	ActiveSolarTileId = TileId;
	ActiveTileLatitude = FMath::Clamp(Latitude, -90.0f, 90.0f);
	ActiveTileLongitude = FMath::Clamp(Longitude, -180.0f, 180.0f);
	ActiveTileTimeZoneId = TimeZoneId;
	ActiveTileUtcOffsetHours = FMath::Clamp(UtcOffsetHours, -12.0f, 14.0f);
	ActiveGrowingSeason.TileId = TileId;
	UpdateSolarTimes();
	UpdateAmbientTemperature();
	return bHasActiveTileSolarData;
}

FAgrarianCalendarSnapshot AAgrarianGameState::GetCalendarSnapshot() const
{
	FAgrarianCalendarSnapshot Snapshot;
	Snapshot.DayOfYear = NormalizeCalendarDay(ActiveDayOfYear, DaysPerAgrarianYear);
	Snapshot.Year = FMath::Max(1, ActiveYear);
	Snapshot.AbsoluteDay = GetAbsoluteAgrarianDay();
	Snapshot.DayOfSeason = GetDayOfSeason(Snapshot.DayOfYear);
	Snapshot.Season = GetSeasonForDay(Snapshot.DayOfYear);
	Snapshot.HourOfDay = FMath::Clamp(WorldHours, 0.0f, 24.0f);
	Snapshot.bInsideGrowingSeason = IsDayInsideActiveGrowingSeason(Snapshot.DayOfYear);
	Snapshot.GrowingSeasonDaysRemaining = GetDaysRemainingInActiveGrowingSeason(Snapshot.DayOfYear);
	return Snapshot;
}

int32 AAgrarianGameState::GetAbsoluteAgrarianDay() const
{
	const int32 SafeYear = FMath::Max(1, ActiveYear);
	const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear);
	return ((SafeYear - 1) * SafeDaysPerYear) + NormalizeCalendarDay(ActiveDayOfYear, SafeDaysPerYear);
}

EAgrarianSeason AAgrarianGameState::GetSeasonForDay(int32 DayOfYear) const
{
	const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear);
	const int32 SeasonalDay = ActiveTileLatitude < 0.0f
		? NormalizeCalendarDay(DayOfYear + (SafeDaysPerYear / 2), SafeDaysPerYear)
		: NormalizeCalendarDay(DayOfYear, SafeDaysPerYear);

	if (SeasonalDay >= 355 || SeasonalDay < 80)
	{
		return EAgrarianSeason::Winter;
	}
	if (SeasonalDay < 172)
	{
		return EAgrarianSeason::Spring;
	}
	if (SeasonalDay < 264)
	{
		return EAgrarianSeason::Summer;
	}
	return EAgrarianSeason::Autumn;
}

int32 AAgrarianGameState::GetDayOfSeason(int32 DayOfYear) const
{
	const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear);
	const int32 SeasonalDay = ActiveTileLatitude < 0.0f
		? NormalizeCalendarDay(DayOfYear + (SafeDaysPerYear / 2), SafeDaysPerYear)
		: NormalizeCalendarDay(DayOfYear, SafeDaysPerYear);

	switch (GetSeasonForDay(DayOfYear))
	{
	case EAgrarianSeason::Spring:
		return SeasonalDay - 79;
	case EAgrarianSeason::Summer:
		return SeasonalDay - 171;
	case EAgrarianSeason::Autumn:
		return SeasonalDay - 263;
	default:
		return SeasonalDay >= 355 ? SeasonalDay - 354 : SeasonalDay + 12;
	}
}

float AAgrarianGameState::ConvertAgrarianDaysToRealHours(float AgrarianDays) const
{
	const float SafeGameHoursPerRealMinute = FMath::Max(0.001f, GameHoursPerRealMinute);
	return FMath::Max(0.0f, AgrarianDays) * (24.0f / SafeGameHoursPerRealMinute) / 60.0f;
}

float AAgrarianGameState::ConvertRealHoursToAgrarianDays(float RealHours) const
{
	return (FMath::Max(0.0f, RealHours) * 60.0f * FMath::Max(0.001f, GameHoursPerRealMinute)) / 24.0f;
}

float AAgrarianGameState::GetLongTaskProgress(int32 StartAbsoluteDay, float StartHourOfDay, float DurationAgrarianDays) const
{
	const float SafeDuration = FMath::Max(0.001f, DurationAgrarianDays);
	const float CurrentAgrarianDay = static_cast<float>(GetAbsoluteAgrarianDay() - 1) + (WorldHours / 24.0f);
	const float StartAgrarianDay = static_cast<float>(FMath::Max(1, StartAbsoluteDay) - 1)
		+ (FMath::Clamp(StartHourOfDay, 0.0f, 24.0f) / 24.0f);
	return FMath::Clamp((CurrentAgrarianDay - StartAgrarianDay) / SafeDuration, 0.0f, 1.0f);
}

bool AAgrarianGameState::IsDayInsideActiveGrowingSeason(int32 DayOfYear) const
{
	return IsDayInRange(
		DayOfYear,
		ActiveGrowingSeason.GrowingSeasonStartDay,
		ActiveGrowingSeason.GrowingSeasonEndDay,
		DaysPerAgrarianYear);
}

int32 AAgrarianGameState::GetDaysRemainingInActiveGrowingSeason(int32 PlantDayOfYear) const
{
	const int32 SafeDaysPerYear = FMath::Max(1, DaysPerAgrarianYear);
	const int32 Day = NormalizeCalendarDay(PlantDayOfYear, SafeDaysPerYear);
	const int32 Start = NormalizeCalendarDay(ActiveGrowingSeason.GrowingSeasonStartDay, SafeDaysPerYear);
	const int32 End = NormalizeCalendarDay(ActiveGrowingSeason.GrowingSeasonEndDay, SafeDaysPerYear);

	if (Start <= End)
	{
		if (Day < Start)
		{
			return End - Start + 1;
		}
		if (Day > End)
		{
			return 0;
		}
		return End - Day + 1;
	}

	if (Day >= Start)
	{
		return (SafeDaysPerYear - Day + 1) + End;
	}
	if (Day <= End)
	{
		return End - Day + 1;
	}
	return 0;
}

FAgrarianCropSeasonAssessment AAgrarianGameState::AssessCropForActiveGrowingSeason(int32 CropMaturityDays, int32 PlantDayOfYear) const
{
	FAgrarianCropSeasonAssessment Assessment;
	Assessment.DaysAvailable = GetDaysRemainingInActiveGrowingSeason(PlantDayOfYear);
	Assessment.SafetyBufferDays = FMath::Max(0, ActiveGrowingSeason.CropSafetyBufferDays);
	Assessment.DaysRequired = FMath::Max(0, CropMaturityDays) + Assessment.SafetyBufferDays;
	Assessment.GrowingZoneLabel = ActiveGrowingSeason.GrowingZoneLabel;
	Assessment.bCanPlantToday = false;

	if (CropMaturityDays <= 0)
	{
		Assessment.Fit = EAgrarianCropSeasonFit::Unknown;
		Assessment.Reason = FText::FromString(TEXT("Crop maturity days must be greater than zero."));
		return Assessment;
	}
	if (!IsDayInsideActiveGrowingSeason(PlantDayOfYear))
	{
		Assessment.Fit = EAgrarianCropSeasonFit::OutOfSeason;
		Assessment.Reason = FText::FromString(TEXT("Planting day is outside the active tile growing season."));
		return Assessment;
	}
	if (Assessment.DaysRequired > ActiveGrowingSeason.FrostFreeDays)
	{
		Assessment.Fit = EAgrarianCropSeasonFit::TooLong;
		Assessment.Reason = FText::FromString(TEXT("Crop maturity is too long for this tile's frost-free growing window."));
		return Assessment;
	}
	if (Assessment.DaysRequired > Assessment.DaysAvailable)
	{
		Assessment.Fit = EAgrarianCropSeasonFit::Marginal;
		Assessment.Reason = FText::FromString(TEXT("Crop can grow in this zone, but this planting date is too late for a reliable harvest."));
		return Assessment;
	}

	Assessment.Fit = EAgrarianCropSeasonFit::FitsSeason;
	Assessment.bCanPlantToday = true;
	Assessment.Reason = FText::FromString(TEXT("Crop maturity fits the active tile growing season."));
	return Assessment;
}

void AAgrarianGameState::OnRep_Weather()
{
	UpdateAmbientTemperature();
}

void AAgrarianGameState::UpdateSolarTimes()
{
	if (ActiveSolarTileId == NAME_None)
	{
		bHasActiveTileSolarData = false;
		SunriseHourLocal = 6.0f;
		SunsetHourLocal = 20.0f;
		SolarNoonHourLocal = 13.0f;
		DayLengthHours = 14.0f;
		return;
	}

	const float ClampedLatitude = FMath::Clamp(ActiveTileLatitude, -89.8f, 89.8f);
	const float ClampedLongitude = FMath::Clamp(ActiveTileLongitude, -180.0f, 180.0f);
	const int32 ClampedDayOfYear = FMath::Clamp(ActiveDayOfYear, 1, 366);
	// NOAA approximate sunrise/sunset model; good enough for MVP regional light timing.
	const double Gamma = (2.0 * PI / 365.0) * (static_cast<double>(ClampedDayOfYear) - 1.0);
	const double EquationOfTimeMinutes = 229.18 * (
		0.000075
		+ 0.001868 * FMath::Cos(Gamma)
		- 0.032077 * FMath::Sin(Gamma)
		- 0.014615 * FMath::Cos(2.0 * Gamma)
		- 0.040849 * FMath::Sin(2.0 * Gamma));
	const double SolarDeclinationRadians =
		0.006918
		- 0.399912 * FMath::Cos(Gamma)
		+ 0.070257 * FMath::Sin(Gamma)
		- 0.006758 * FMath::Cos(2.0 * Gamma)
		+ 0.000907 * FMath::Sin(2.0 * Gamma)
		- 0.002697 * FMath::Cos(3.0 * Gamma)
		+ 0.00148 * FMath::Sin(3.0 * Gamma);

	const double LatitudeRadians = FMath::DegreesToRadians(static_cast<double>(ClampedLatitude));
	const double SolarZenithRadians = FMath::DegreesToRadians(90.833);
	const double HourAngleArgument =
		(FMath::Cos(SolarZenithRadians) / (FMath::Cos(LatitudeRadians) * FMath::Cos(SolarDeclinationRadians)))
		- FMath::Tan(LatitudeRadians) * FMath::Tan(SolarDeclinationRadians);

	if (HourAngleArgument <= -1.0 || HourAngleArgument >= 1.0)
	{
		bHasActiveTileSolarData = true;
		SolarNoonHourLocal = FMath::Fmod(12.0f + ActiveTileUtcOffsetHours - (ClampedLongitude / 15.0f), 24.0f);
		if (SolarNoonHourLocal < 0.0f)
		{
			SolarNoonHourLocal += 24.0f;
		}
		DayLengthHours = HourAngleArgument <= -1.0 ? 24.0f : 0.0f;
		SunriseHourLocal = DayLengthHours >= 24.0f ? 0.0f : SolarNoonHourLocal;
		SunsetHourLocal = DayLengthHours >= 24.0f ? 24.0f : SolarNoonHourLocal;
		return;
	}

	const double HourAngleDegrees = FMath::RadiansToDegrees(FMath::Acos(HourAngleArgument));
	const double SolarNoonMinutes = 720.0 - (4.0 * ClampedLongitude) - EquationOfTimeMinutes + (ActiveTileUtcOffsetHours * 60.0);
	const double SunriseMinutes = SolarNoonMinutes - (HourAngleDegrees * 4.0);
	const double SunsetMinutes = SolarNoonMinutes + (HourAngleDegrees * 4.0);

	auto NormalizeHour = [](double Minutes)
	{
		double Hours = FMath::Fmod(Minutes / 60.0, 24.0);
		if (Hours < 0.0)
		{
			Hours += 24.0;
		}
		return static_cast<float>(Hours);
	};

	bHasActiveTileSolarData = true;
	SunriseHourLocal = NormalizeHour(SunriseMinutes);
	SunsetHourLocal = NormalizeHour(SunsetMinutes);
	SolarNoonHourLocal = NormalizeHour(SolarNoonMinutes);
	DayLengthHours = static_cast<float>((SunsetMinutes - SunriseMinutes) / 60.0);
}

void AAgrarianGameState::UpdateAmbientTemperature()
{
	const float ClearSkyTemperatureC = GetClearSkyTemperatureForHour(WorldHours);
	const float DailyMeanTemperatureC = (RegionalDailyLowTemperatureC + RegionalDailyHighTemperatureC) * 0.5f;
	const float ObservedAnchoredTemperatureC = bHasRegionalObservedTemperature
		? RegionalObservedTemperatureC + (ClearSkyTemperatureC - DailyMeanTemperatureC)
		: ClearSkyTemperatureC;
	const float BaseTemperatureC = bHasRegionalObservedTemperature
		? FMath::Lerp(ClearSkyTemperatureC, ObservedAnchoredTemperatureC, ObservedTemperatureBlend)
		: ClearSkyTemperatureC;
	float WeatherModifier = 0.0f;

	switch (Weather)
	{
	case EAgrarianWeatherType::Rain:
		WeatherModifier = -2.0f;
		break;
	case EAgrarianWeatherType::ColdWind:
		WeatherModifier = -8.0f;
		break;
	case EAgrarianWeatherType::Storm:
		WeatherModifier = -5.0f;
		break;
	default:
		WeatherModifier = 0.0f;
		break;
	}

	AmbientTemperatureC = FMath::Clamp(BaseTemperatureC + WeatherModifier, -80.0f, 70.0f);
}
