package basic

import (
	"math"

	"b612.me/astro/planet"
	. "b612.me/astro/tools"
)

const astronomicalUnitLightTimeDays = 0.0057755183

type planetPhysicalModel struct {
	planetIndex      int
	positiveEast     bool
	equatorialRadius float64
	polarRadius      float64
	poleRotation     func(float64) (ra, dec, rotationEast float64)
}

type phaseAngleModel struct {
	base      float64
	rate      float64
	accelRate float64
}

// PlanetPhysicalInfo 行星物理观测参数 / planetary physical observing parameters.
type PlanetPhysicalInfo struct {
	// SubEarthLongitude 子地经度，单位度；正方向遵循该天体当前 IAU/Horizons 制图约定。
	SubEarthLongitude float64
	// SubEarthLatitude 子地纬度，单位度。
	SubEarthLatitude float64
	// SubSolarLongitude 子日经度，单位度；正方向遵循该天体当前 IAU/Horizons 制图约定。
	SubSolarLongitude float64
	// SubSolarLatitude 子日纬度，单位度。
	SubSolarLatitude float64
	// NorthPolePositionAngle 天体北极位置角，单位度。
	NorthPolePositionAngle float64
}

var (
	mercuryPhysicalModel = planetPhysicalModel{
		planetIndex:      1,
		positiveEast:     false,
		equatorialRadius: 2440.53,
		polarRadius:      2438.26,
		poleRotation:     mercuryPoleRotation,
	}
	venusPhysicalModel = planetPhysicalModel{
		planetIndex:      2,
		positiveEast:     true,
		equatorialRadius: 6051.8,
		polarRadius:      6051.8,
		poleRotation:     venusPoleRotation,
	}
	marsPhysicalModel = planetPhysicalModel{
		planetIndex:      3,
		positiveEast:     false,
		equatorialRadius: 3396.19,
		polarRadius:      3376.20,
		poleRotation:     marsPoleRotation,
	}
	jupiterPhysicalModel = planetPhysicalModel{
		planetIndex:      4,
		positiveEast:     false,
		equatorialRadius: 71492.0,
		polarRadius:      66854.0,
		poleRotation:     jupiterPoleRotation,
	}
	saturnPhysicalModel = planetPhysicalModel{
		planetIndex:      5,
		positiveEast:     false,
		equatorialRadius: 60268.0,
		polarRadius:      54364.0,
		poleRotation:     saturnPoleRotation,
	}
	uranusPhysicalModel = planetPhysicalModel{
		planetIndex:      6,
		positiveEast:     true,
		equatorialRadius: 25559.0,
		polarRadius:      24973.0,
		poleRotation:     uranusPoleRotation,
	}
	neptunePhysicalModel = planetPhysicalModel{
		planetIndex:      7,
		positiveEast:     false,
		equatorialRadius: 24764.0,
		polarRadius:      24341.0,
		poleRotation:     neptunePoleRotation,
	}
)

// MercuryPhysical 水星物理观测参数 / physical observing parameters of Mercury.
func MercuryPhysical(jd float64) PlanetPhysicalInfo {
	return MercuryPhysicalN(jd, -1)
}

// MercuryPhysicalN 水星物理观测参数（截断版） / truncated physical observing parameters of Mercury.
func MercuryPhysicalN(jd float64, n int) PlanetPhysicalInfo {
	return planetPhysicalN(jd, n, mercuryPhysicalModel)
}

// VenusPhysical 金星物理观测参数 / physical observing parameters of Venus.
func VenusPhysical(jd float64) PlanetPhysicalInfo {
	return VenusPhysicalN(jd, -1)
}

// VenusPhysicalN 金星物理观测参数（截断版） / truncated physical observing parameters of Venus.
func VenusPhysicalN(jd float64, n int) PlanetPhysicalInfo {
	return planetPhysicalN(jd, n, venusPhysicalModel)
}

// MarsPhysical 火星物理观测参数 / physical observing parameters of Mars.
func MarsPhysical(jd float64) PlanetPhysicalInfo {
	return MarsPhysicalN(jd, -1)
}

// MarsPhysicalN 火星物理观测参数（截断版） / truncated physical observing parameters of Mars.
func MarsPhysicalN(jd float64, n int) PlanetPhysicalInfo {
	return planetPhysicalN(jd, n, marsPhysicalModel)
}

// JupiterPhysical 木星物理观测参数 / physical observing parameters of Jupiter.
func JupiterPhysical(jd float64) PlanetPhysicalInfo {
	return JupiterPhysicalN(jd, -1)
}

// JupiterPhysicalN 木星物理观测参数（截断版） / truncated physical observing parameters of Jupiter.
func JupiterPhysicalN(jd float64, n int) PlanetPhysicalInfo {
	return planetPhysicalN(jd, n, jupiterPhysicalModel)
}

// SaturnPhysical 土星物理观测参数 / physical observing parameters of Saturn.
func SaturnPhysical(jd float64) PlanetPhysicalInfo {
	return SaturnPhysicalN(jd, -1)
}

// SaturnPhysicalN 土星物理观测参数（截断版） / truncated physical observing parameters of Saturn.
func SaturnPhysicalN(jd float64, n int) PlanetPhysicalInfo {
	return planetPhysicalN(jd, n, saturnPhysicalModel)
}

// UranusPhysical 天王星物理观测参数 / physical observing parameters of Uranus.
func UranusPhysical(jd float64) PlanetPhysicalInfo {
	return UranusPhysicalN(jd, -1)
}

// UranusPhysicalN 天王星物理观测参数（截断版） / truncated physical observing parameters of Uranus.
func UranusPhysicalN(jd float64, n int) PlanetPhysicalInfo {
	return planetPhysicalN(jd, n, uranusPhysicalModel)
}

// NeptunePhysical 海王星物理观测参数 / physical observing parameters of Neptune.
func NeptunePhysical(jd float64) PlanetPhysicalInfo {
	return NeptunePhysicalN(jd, -1)
}

// NeptunePhysicalN 海王星物理观测参数（截断版） / truncated physical observing parameters of Neptune.
func NeptunePhysicalN(jd float64, n int) PlanetPhysicalInfo {
	return planetPhysicalN(jd, n, neptunePhysicalModel)
}

func planetPhysicalN(jd float64, n int, model planetPhysicalModel) PlanetPhysicalInfo {
	initialX, initialY, initialZ := planetXYZN(model.planetIndex, jd, n)
	targetVector := Vector3{initialX, initialY, initialZ}
	lightTimeDays := astronomicalUnitLightTimeDays * vectorMagnitude(targetVector)
	targetJD := jd - lightTimeDays

	geoX, geoY, geoZ := planetXYZN(model.planetIndex, targetJD, n)
	geocentricVector := Vector3{geoX, geoY, geoZ}
	observerDirection := normalizeVector(Vector3{-geocentricVector[0], -geocentricVector[1], -geocentricVector[2]})

	heliocentricLongitude := planet.WherePlanetN(model.planetIndex, 0, targetJD, n)
	heliocentricLatitude := planet.WherePlanetN(model.planetIndex, 1, targetJD, n)
	solarDirection := normalizeVector(eclipticCartesian(heliocentricLongitude+180, -heliocentricLatitude, 1))

	obliquity := EclipticObliquity(targetJD, false)
	observerEquatorial := normalizeVector(rotateEclipticToEquatorial(observerDirection, obliquity))
	solarEquatorial := normalizeVector(rotateEclipticToEquatorial(solarDirection, obliquity))

	poleRA, poleDec, rotationEast := model.poleRotation(targetJD)
	poleJ2000 := raDecToVector(poleRA, poleDec)
	nodeJ2000 := Vector3{-math.Sin(poleRA * rad), math.Cos(poleRA * rad), 0}
	eastJ2000 := normalizeVector(pxp(poleJ2000, nodeJ2000))
	primeMeridianJ2000 := normalizeVector(Vector3{
		nodeJ2000[0]*Cos(rotationEast) + eastJ2000[0]*Sin(rotationEast),
		nodeJ2000[1]*Cos(rotationEast) + eastJ2000[1]*Sin(rotationEast),
		nodeJ2000[2]*Cos(rotationEast) + eastJ2000[2]*Sin(rotationEast),
	})

	j2000ToDate := precessionMatrix(2451545.0, targetJD)
	poleDate := normalizeVector(applyMatrix3(j2000ToDate, poleJ2000))
	primeMeridianDate := normalizeVector(applyMatrix3(j2000ToDate, primeMeridianJ2000))
	eastDate := normalizeVector(pxp(poleDate, primeMeridianDate))

	poleRAOfDate, poleDecOfDate := vectorToRaDec(poleDate)
	planetRA, planetDec := vectorToRaDec(observerEquatorial)

	return PlanetPhysicalInfo{
		SubEarthLongitude:      bodyLongitude(observerEquatorial, primeMeridianDate, eastDate, model.positiveEast),
		SubEarthLatitude:       bodyLatitude(observerEquatorial, poleDate, model.equatorialRadius, model.polarRadius),
		SubSolarLongitude:      bodyLongitude(solarEquatorial, primeMeridianDate, eastDate, model.positiveEast),
		SubSolarLatitude:       bodyLatitude(solarEquatorial, poleDate, model.equatorialRadius, model.polarRadius),
		NorthPolePositionAngle: northPolePositionAngle(planetRA, planetDec, poleRAOfDate, poleDecOfDate),
	}
}

func bodyLongitude(direction, primeMeridian, eastAxis Vector3, positiveEast bool) float64 {
	eastLongitude := Limit360(math.Atan2(vectorDot(direction, eastAxis), vectorDot(direction, primeMeridian)) * deg)
	if positiveEast {
		return eastLongitude
	}
	return Limit360(360 - eastLongitude)
}

func bodyLatitude(direction, pole Vector3, equatorialRadius, polarRadius float64) float64 {
	geocentricLatitude := ArcSin(vectorDot(direction, pole))
	if equatorialRadius == polarRadius {
		return geocentricLatitude
	}
	ratio := (polarRadius * polarRadius) / (equatorialRadius * equatorialRadius)
	return math.Atan2(math.Sin(geocentricLatitude*rad), math.Cos(geocentricLatitude*rad)*ratio) * deg
}

func northPolePositionAngle(planetRA, planetDec, poleRA, poleDec float64) float64 {
	y := math.Cos(poleDec*rad) * math.Sin((poleRA-planetRA)*rad)
	x := math.Sin(poleDec*rad)*math.Cos(planetDec*rad) -
		math.Cos(poleDec*rad)*math.Sin(planetDec*rad)*math.Cos((poleRA-planetRA)*rad)
	return Limit360(-math.Atan2(y, x) * deg)
}

func vectorDot(a, b Vector3) float64 {
	return a[0]*b[0] + a[1]*b[1] + a[2]*b[2]
}

func vectorMagnitude(vector Vector3) float64 {
	return math.Sqrt(vectorDot(vector, vector))
}

func normalizeVector(vector Vector3) Vector3 {
	normalized, magnitude := pn(vector)
	if magnitude == 0 {
		return Vector3{}
	}
	return normalized
}

func mercuryPoleRotation(jd float64) (ra, dec, rotationEast float64) {
	days := jd - 2451545.0
	julianCentury := days / 36525.0
	m1 := Limit360(174.7910857 + 4.092335*days)
	m2 := Limit360(349.5821714 + 8.184670*days)
	m3 := Limit360(164.3732571 + 12.277005*days)
	m4 := Limit360(339.1643429 + 16.369340*days)
	m5 := Limit360(153.9554286 + 20.461675*days)
	ra = 281.0103 - 0.0328*julianCentury
	dec = 61.4155 - 0.0049*julianCentury
	rotationEast = 329.5988 + 6.1385108*days +
		0.01067257*Sin(m1) -
		0.00112309*Sin(m2) -
		0.00011040*Sin(m3) -
		0.00002539*Sin(m4) -
		0.00000571*Sin(m5)
	return
}

func venusPoleRotation(jd float64) (ra, dec, rotationEast float64) {
	days := jd - 2451545.0
	return 272.76, 67.16, 160.20 - 1.4813688*days
}

// Mars/Jupiter orientation terms follow the official NAIF text PCK `pck00011.tpc`.
var (
	marsPoleRAAngles = [...]phaseAngleModel{
		{198.991226, 19139.4819985, 0},
		{226.292679, 38280.8511281, 0},
		{249.663391, 57420.7251593, 0},
		{266.183510, 76560.6367950, 0},
		{79.398797, 0.5042615, 0},
	}
	marsPoleRACoefficients = [...]float64{0.000068, 0.000238, 0.000052, 0.000009, 0.419057}
	marsPoleDECAngles      = [...]phaseAngleModel{
		{122.433576, 19139.9407476, 0},
		{43.058401, 38280.8753272, 0},
		{57.663379, 57420.7517205, 0},
		{79.476401, 76560.6495004, 0},
		{166.325722, 0.5042615, 0},
	}
	marsPoleDECCoefficients = [...]float64{0.000051, 0.000141, 0.000031, 0.000005, 1.591274}
	marsPrimeMeridianAngles = [...]phaseAngleModel{
		{129.071773, 19140.0328244, 0},
		{36.352167, 38281.0473591, 0},
		{56.668646, 57420.9295360, 0},
		{67.364003, 76560.2552215, 0},
		{104.792680, 95700.4387578, 0},
		{95.391654, 0.5042615, 0},
	}
	marsPrimeMeridianCoefficients = [...]float64{0.000145, 0.000157, 0.000040, 0.000001, 0.000001, 0.584542}
	jupiterPoleAngles             = [...]phaseAngleModel{
		{99.360714, 4850.4046, 0},
		{175.895369, 1191.9605, 0},
		{300.323162, 262.5475, 0},
		{114.012305, 6070.2476, 0},
		{49.511251, 64.3000, 0},
	}
	jupiterPoleRACoefficients  = [...]float64{0.000117, 0.000938, 0.001432, 0.000030, 0.002150}
	jupiterPoleDECCoefficients = [...]float64{0.000050, 0.000404, 0.000617, -0.000013, 0.000926}
)

func marsPoleRotation(jd float64) (ra, dec, rotationEast float64) {
	days := jd - 2451545.0
	julianCentury := days / 36525.0
	ra = 317.269202 - 0.10927547*julianCentury + sumSinOrientationTerms(marsPoleRAAngles[:], marsPoleRACoefficients[:], julianCentury)
	dec = 54.432516 - 0.05827105*julianCentury + sumCosOrientationTerms(marsPoleDECAngles[:], marsPoleDECCoefficients[:], julianCentury)
	rotationEast = 176.049863 + 350.891982443297*days +
		sumSinOrientationTerms(marsPrimeMeridianAngles[:], marsPrimeMeridianCoefficients[:], julianCentury)
	return
}

func jupiterPoleRotation(jd float64) (ra, dec, rotationEast float64) {
	days := jd - 2451545.0
	julianCentury := days / 36525.0
	ra = 268.056595 - 0.006499*julianCentury + sumSinOrientationTerms(jupiterPoleAngles[:], jupiterPoleRACoefficients[:], julianCentury)
	dec = 64.495303 + 0.002413*julianCentury + sumCosOrientationTerms(jupiterPoleAngles[:], jupiterPoleDECCoefficients[:], julianCentury)
	rotationEast = 284.95 + 870.5360000*days
	return
}

func saturnPoleRotation(jd float64) (ra, dec, rotationEast float64) {
	days := jd - 2451545.0
	julianCentury := days / 36525.0
	ra = 40.589 - 0.036*julianCentury
	dec = 83.537 - 0.004*julianCentury
	rotationEast = 38.90 + 810.7939024*days
	return
}

func uranusPoleRotation(jd float64) (ra, dec, rotationEast float64) {
	days := jd - 2451545.0
	return 257.311, -15.175, 203.81 - 501.1600928*days
}

func neptunePoleRotation(jd float64) (ra, dec, rotationEast float64) {
	days := jd - 2451545.0
	julianCentury := days / 36525.0
	n := Limit360(357.85 + 52.316*julianCentury)
	ra = 299.36 + 0.70*Sin(n)
	dec = 43.46 - 0.51*Cos(n)
	rotationEast = 249.978 + 541.1397757*days - 0.48*Sin(n)
	return
}

func (model phaseAngleModel) at(julianCentury float64) float64 {
	return Limit360(model.base + model.rate*julianCentury + model.accelRate*julianCentury*julianCentury)
}

func sumSinOrientationTerms(angles []phaseAngleModel, coefficients []float64, julianCentury float64) float64 {
	sum := 0.0
	for i, angle := range angles {
		sum += coefficients[i] * Sin(angle.at(julianCentury))
	}
	return sum
}

func sumCosOrientationTerms(angles []phaseAngleModel, coefficients []float64, julianCentury float64) float64 {
	sum := 0.0
	for i, angle := range angles {
		sum += coefficients[i] * Cos(angle.at(julianCentury))
	}
	return sum
}
