package internal

import (
	"errors"
	"math"

	. "b612.me/astro/tools"
)

const (
	SynodicMonthDays = 29.530588853
)

var (
	ErrNeverRise     = errors.New("rise event does not occur on this date")
	ErrNeverSet      = errors.New("set event does not occur on this date")
	ErrNotOnThisDate = errors.New("rise/set event occurs on adjacent date")
)

func MeanObliquity(jd float64) float64 {
	t := (jd - 2451545.0) / 36525.0
	return 23.4392911111 - (46.8150*t+0.00059*t*t-0.001813*t*t*t)/3600.0
}

func MeanSiderealTime(jd float64) float64 {
	t := (jd - 2451545.0) / 36525.0
	return Limit360(280.46061837 + 360.98564736629*(jd-2451545.0) + 0.000387933*t*t - t*t*t/38710000.0)
}

func EclipticToEquatorial(jd, lo, bo float64) (float64, float64) {
	eps := MeanObliquity(jd)
	ra := math.Atan2(Sin(lo)*Cos(eps)-Tan(bo)*Sin(eps), Cos(lo)) * 180.0 / math.Pi
	if ra < 0 {
		ra += 360
	}
	dec := ArcSin(Sin(bo)*Cos(eps) + Cos(bo)*Sin(eps)*Sin(lo))
	return ra, dec
}

func HorizontalCoordinates(ra, dec, jd, lon, lat float64) (float64, float64, float64) {
	lst := Limit360(MeanSiderealTime(jd) + lon)
	hourAngle := Limit360(lst - ra)
	altitude := ArcSin(clampUnit(Sin(lat)*Sin(dec) + Cos(lat)*Cos(dec)*Cos(hourAngle)))

	y := Sin(hourAngle)
	x := Cos(hourAngle)*Sin(lat) - Tan(dec)*Cos(lat)
	azimuth := math.Atan2(y, x) * 180.0 / math.Pi
	if azimuth < 0 {
		if hourAngle < 180 {
			azimuth += 360
		} else {
			azimuth += 180
		}
	} else if hourAngle < 180 {
		azimuth += 180
	}
	return altitude, Limit360(azimuth), hourAngle
}

func TopocentricRaDec(ra, dec, observerLat, observerLon, jd, distanceEarthRadii, heightMeters float64) (float64, float64) {
	u := math.Atan(0.99664719 * Tan(observerLat))
	rhoSin := 0.99664719*math.Sin(u) + heightMeters/6378140.0*Sin(observerLat)
	rhoCos := math.Cos(u) + heightMeters/6378140.0*Cos(observerLat)
	parallax := math.Asin(1.0 / distanceEarthRadii)

	hourAngle := (Limit360(MeanSiderealTime(jd) + observerLon - ra)) * math.Pi / 180.0
	decRad := dec * math.Pi / 180.0

	numerator := -rhoCos * math.Sin(parallax) * math.Sin(hourAngle)
	denominator := math.Cos(decRad) - rhoCos*math.Sin(parallax)*math.Cos(hourAngle)
	deltaRA := math.Atan2(numerator, denominator)

	topRA := Limit360(ra + deltaRA*180.0/math.Pi)
	topDec := math.Atan2((math.Sin(decRad)-rhoSin*math.Sin(parallax))*math.Cos(deltaRA), denominator) * 180.0 / math.Pi
	return topRA, topDec
}

func SearchRiseSet(startJD, targetAltitude, stepMinutes float64, isRise bool, altitudeFn func(float64) float64) (float64, error) {
	step := stepMinutes / 1440.0
	prevJD := startJD
	prevAlt := altitudeFn(prevJD) - targetAltitude
	minAlt := prevAlt
	maxAlt := prevAlt

	for i := 1; i <= int(math.Round(1440.0/stepMinutes)); i++ {
		currentJD := startJD + float64(i)*step
		currentAlt := altitudeFn(currentJD) - targetAltitude
		if currentAlt < minAlt {
			minAlt = currentAlt
		}
		if currentAlt > maxAlt {
			maxAlt = currentAlt
		}
		if crosses(prevAlt, currentAlt, isRise) {
			return bisectEvent(prevJD, currentJD, targetAltitude, altitudeFn), nil
		}
		prevJD = currentJD
		prevAlt = currentAlt
	}

	if maxAlt < 0 {
		return 0, ErrNeverRise
	}
	if minAlt > 0 {
		return 0, ErrNeverSet
	}
	return 0, ErrNotOnThisDate
}

func crosses(prevAlt, currentAlt float64, isRise bool) bool {
	if isRise {
		return prevAlt < 0 && currentAlt >= 0
	}
	return prevAlt > 0 && currentAlt <= 0
}

func bisectEvent(lo, hi, targetAltitude float64, altitudeFn func(float64) float64) float64 {
	loAlt := altitudeFn(lo) - targetAltitude
	for i := 0; i < 40; i++ {
		mid := (lo + hi) / 2.0
		midAlt := altitudeFn(mid) - targetAltitude
		if midAlt == 0 {
			return mid
		}
		if sameSign(loAlt, midAlt) {
			lo = mid
			loAlt = midAlt
		} else {
			hi = mid
		}
	}
	return (lo + hi) / 2.0
}

func sameSign(a, b float64) bool {
	return (a >= 0 && b >= 0) || (a <= 0 && b <= 0)
}

func clampUnit(v float64) float64 {
	if v > 1 {
		return 1
	}
	if v < -1 {
		return -1
	}
	return v
}
