package wayland

import (
	"math"
	"time"
)

const (
	degToRad = math.Pi / 180.0
	radToDeg = 180.0 / math.Pi
)

type SunCondition int

const (
	SunNormal SunCondition = iota
	SunMidnightSun
	SunPolarNight
)

type SunTimes struct {
	Dawn    time.Time
	Sunrise time.Time
	Sunset  time.Time
	Night   time.Time
}

func daysInYear(year int) int {
	if (year%4 == 0 && year%100 != 0) || year%400 == 0 {
		return 366
	}
	return 365
}

func dateOrbitAngle(t time.Time) float64 {
	return 2 * math.Pi / float64(daysInYear(t.Year())) * float64(t.YearDay()-1)
}

func equationOfTime(orbitAngle float64) float64 {
	return 4 * (0.000075 +
		0.001868*math.Cos(orbitAngle) -
		0.032077*math.Sin(orbitAngle) -
		0.014615*math.Cos(2*orbitAngle) -
		0.040849*math.Sin(2*orbitAngle))
}

func sunDeclination(orbitAngle float64) float64 {
	return 0.006918 -
		0.399912*math.Cos(orbitAngle) +
		0.070257*math.Sin(orbitAngle) -
		0.006758*math.Cos(2*orbitAngle) +
		0.000907*math.Sin(2*orbitAngle) -
		0.002697*math.Cos(3*orbitAngle) +
		0.00148*math.Sin(3*orbitAngle)
}

func sunHourAngle(latRad, declination, targetSunRad float64) float64 {
	return math.Acos(math.Cos(targetSunRad)/
		math.Cos(latRad)*math.Cos(declination) -
		math.Tan(latRad)*math.Tan(declination))
}

func hourAngleToSeconds(hourAngle, eqtime float64) float64 {
	return radToDeg * (4.0*math.Pi - 4*hourAngle - eqtime) * 60
}

func sunCondition(latRad, declination float64) SunCondition {
	signLat := latRad >= 0
	signDecl := declination >= 0
	if signLat == signDecl {
		return SunMidnightSun
	}
	return SunPolarNight
}

func CalculateSunTimesWithTwilight(lat, lon float64, date time.Time, elevTwilight, elevDaylight float64) (SunTimes, SunCondition) {
	latRad := lat * degToRad
	elevTwilightRad := (90.833 - elevTwilight) * degToRad
	elevDaylightRad := (90.833 - elevDaylight) * degToRad

	utc := date.UTC()
	orbitAngle := dateOrbitAngle(utc)
	decl := sunDeclination(orbitAngle)
	eqtime := equationOfTime(orbitAngle)

	haTwilight := sunHourAngle(latRad, decl, elevTwilightRad)
	haDaylight := sunHourAngle(latRad, decl, elevDaylightRad)

	if math.IsNaN(haTwilight) || math.IsNaN(haDaylight) {
		cond := sunCondition(latRad, decl)
		return SunTimes{}, cond
	}

	dayStart := time.Date(utc.Year(), utc.Month(), utc.Day(), 0, 0, 0, 0, time.UTC)
	lonOffset := time.Duration(-lon*4) * time.Minute

	dawnSecs := hourAngleToSeconds(math.Abs(haTwilight), eqtime)
	sunriseSecs := hourAngleToSeconds(math.Abs(haDaylight), eqtime)
	sunsetSecs := hourAngleToSeconds(-math.Abs(haDaylight), eqtime)
	nightSecs := hourAngleToSeconds(-math.Abs(haTwilight), eqtime)

	return SunTimes{
		Dawn:    dayStart.Add(time.Duration(dawnSecs)*time.Second + lonOffset).In(date.Location()),
		Sunrise: dayStart.Add(time.Duration(sunriseSecs)*time.Second + lonOffset).In(date.Location()),
		Sunset:  dayStart.Add(time.Duration(sunsetSecs)*time.Second + lonOffset).In(date.Location()),
		Night:   dayStart.Add(time.Duration(nightSecs)*time.Second + lonOffset).In(date.Location()),
	}, SunNormal
}

func CalculateSunTimes(lat, lon float64, date time.Time) SunTimes {
	times, cond := CalculateSunTimesWithTwilight(lat, lon, date, -6.0, 3.0)
	switch cond {
	case SunMidnightSun:
		dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
		dayEnd := dayStart.Add(24*time.Hour - time.Second)
		return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayEnd, Night: dayEnd}
	case SunPolarNight:
		dayStart := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
		return SunTimes{Dawn: dayStart, Sunrise: dayStart, Sunset: dayStart, Night: dayStart}
	}
	return times
}
