package coord

import "math"

// Galactic 银道坐标 / galactic coordinates.
type Galactic struct {
	Lon float64 // 银经，单位度 / galactic longitude in degrees.
	Lat float64 // 银纬，单位度 / galactic latitude in degrees.
}

// SOFA ICRS2G/G2ICRS fixed rotation matrix.
// Source: IAU SOFA 2023-10-11, iauIcrs2g/iauG2icrs.
var icrsToGalacticMatrix = [3][3]float64{
	{-0.054875560416215368492398900454, -0.873437090234885048760383168409, -0.483835015548713226831774175116},
	{+0.494109427875583673525222371358, -0.444829629960011178146614061616, +0.746982244497218890527388004556},
	{-0.867666149019004701181616534570, -0.198076373431201528180486091412, +0.455983776175066922272100478348},
}

// EclipticToEquatorialByObliquity 黄道转赤道坐标（指定黄赤交角） / ecliptic to equatorial by obliquity.
//
//	lon: 黄经，单位度
//	lat: 黄纬，单位度
//	obliquity: 黄赤交角，单位度
//
// 返回：
//
//	赤经 RA，单位度；赤纬 Dec，单位度
//
// Returns right ascension and declination in degrees for the supplied ecliptic longitude, latitude, and obliquity.
func EclipticToEquatorialByObliquity(lon, lat, obliquity float64) Equatorial {
	sinLon, cosLon := sinCosDeg(lon)
	sinLat, cosLat := sinCosDeg(lat)
	sinObliquity, cosObliquity := sinCosDeg(obliquity)

	ra := normalize360(math.Atan2(sinLon*cosObliquity-math.Tan(lat*math.Pi/180)*sinObliquity, cosLon) * 180 / math.Pi)
	dec := math.Asin(clampUnit(sinLat*cosObliquity+cosLat*sinObliquity*sinLon)) * 180 / math.Pi
	return Equatorial{RA: ra, Dec: dec}
}

// EquatorialToEclipticByObliquity 赤道转黄道坐标（指定黄赤交角） / equatorial to ecliptic by obliquity.
//
//	ra: 赤经，单位度
//	dec: 赤纬，单位度
//	obliquity: 黄赤交角，单位度
//
// 返回：
//
//	黄经 Lon，单位度；黄纬 Lat，单位度
//
// Returns ecliptic longitude and latitude in degrees for the supplied right ascension, declination, and obliquity.
func EquatorialToEclipticByObliquity(ra, dec, obliquity float64) Ecliptic {
	sinRA, cosRA := sinCosDeg(ra)
	sinDec, cosDec := sinCosDeg(dec)
	sinObliquity, cosObliquity := sinCosDeg(obliquity)

	lon := normalize360(math.Atan2(sinRA*cosObliquity+math.Tan(dec*math.Pi/180)*sinObliquity, cosRA) * 180 / math.Pi)
	lat := math.Asin(clampUnit(sinDec*cosObliquity-cosDec*sinObliquity*sinRA)) * 180 / math.Pi
	return Ecliptic{Lon: lon, Lat: lat}
}

// HourAngleDeclinationToHorizontal 时角赤纬转地平坐标 / horizontal coordinates from hour angle and declination.
//
//	hourAngle: 时角，单位度
//	declination: 赤纬，单位度
//	latitude: 观测者地理纬度，单位度，北正南负
//
// 返回：
//
//	方位角 Azimuth（正北为0，顺时针增加）、高度角 Altitude、天顶距 Zenith，均为度
//
// Returns azimuth, altitude, and zenith distance in degrees from the supplied hour angle, declination, and site latitude.
func HourAngleDeclinationToHorizontal(hourAngle, declination, latitude float64) Horizontal {
	sinLatitude, cosLatitude := sinCosDeg(latitude)
	sinDeclination, cosDeclination := sinCosDeg(declination)
	sinHourAngle, cosHourAngle := sinCosDeg(hourAngle)

	altitude := math.Asin(clampUnit(sinLatitude*sinDeclination+cosLatitude*cosDeclination*cosHourAngle)) * 180 / math.Pi
	azimuth := normalize360(math.Atan2(-cosDeclination*sinHourAngle, cosLatitude*sinDeclination-sinLatitude*cosDeclination*cosHourAngle) * 180 / math.Pi)
	return Horizontal{
		Azimuth:   azimuth,
		Altitude:  altitude,
		Zenith:    90 - altitude,
		HourAngle: normalize360(hourAngle),
	}
}

// HorizontalToHourAngleDeclination 地平坐标转时角赤纬 / hour angle and declination from horizontal coordinates.
//
//	azimuth: 方位角，单位度，正北为0，顺时针增加
//	altitude: 高度角，单位度
//	latitude: 观测者地理纬度，单位度，北正南负
//
// 返回：
//
//	时角 HourAngle，单位度；赤纬 Declination，单位度
//
// Returns hour angle and declination in degrees from the supplied horizontal coordinates and site latitude.
func HorizontalToHourAngleDeclination(azimuth, altitude, latitude float64) (hourAngle, declination float64) {
	sinLatitude, cosLatitude := sinCosDeg(latitude)
	sinAltitude, cosAltitude := sinCosDeg(altitude)
	sinAzimuth, cosAzimuth := sinCosDeg(azimuth)

	declination = math.Asin(clampUnit(sinLatitude*sinAltitude+cosLatitude*cosAltitude*cosAzimuth)) * 180 / math.Pi
	sinHourAngle := -cosAltitude * sinAzimuth
	cosHourAngle := sinAltitude*cosLatitude - cosAltitude*sinLatitude*cosAzimuth
	hourAngle = normalize360(math.Atan2(sinHourAngle, cosHourAngle) * 180 / math.Pi)
	return hourAngle, declination
}

// EquatorialToHorizontalByLocalSiderealTime 赤道转地平坐标（指定地方恒星时） / equatorial to horizontal by local sidereal time.
//
//	localSiderealTimeHours: 站点本地恒星时，单位小时
//	ra: 赤经，单位度
//	dec: 赤纬，单位度
//	latitude: 观测者地理纬度，单位度，北正南负
//
// 返回：
//
//	方位角 Azimuth（正北为0，顺时针增加）、高度角 Altitude、天顶距 Zenith，均为度；
//	附带返回对应的时角 HourAngle，单位度
//
// Returns horizontal coordinates in degrees from local sidereal time, right ascension, declination, and site latitude.
//
// 例：
//
//	hz := coord.EquatorialToHorizontalByLocalSiderealTime(10.5, 83.6331, 22.0145, 31.2)
func EquatorialToHorizontalByLocalSiderealTime(localSiderealTimeHours, ra, dec, latitude float64) Horizontal {
	hourAngle := normalize360(localSiderealTimeHours*15 - ra)
	return HourAngleDeclinationToHorizontal(hourAngle, dec, latitude)
}

// HorizontalToEquatorialByLocalSiderealTime 地平转赤道坐标（指定地方恒星时） / horizontal to equatorial by local sidereal time.
//
//	localSiderealTimeHours: 站点本地恒星时，单位小时
//	azimuth: 方位角，单位度，正北为0，顺时针增加
//	altitude: 高度角，单位度
//	latitude: 观测者地理纬度，单位度，北正南负
//
// 返回：
//
//	赤经 RA，单位度；赤纬 Dec，单位度
//
// Returns right ascension and declination in degrees from local sidereal time and the supplied horizontal coordinates.
//
// 例：
//
//	eq := coord.HorizontalToEquatorialByLocalSiderealTime(10.5, 128.2, 37.6, 31.2)
func HorizontalToEquatorialByLocalSiderealTime(localSiderealTimeHours, azimuth, altitude, latitude float64) Equatorial {
	hourAngle, declination := HorizontalToHourAngleDeclination(azimuth, altitude, latitude)
	ra := normalize360(localSiderealTimeHours*15 - hourAngle)
	return Equatorial{RA: ra, Dec: declination}
}

// EquatorialToGalactic 赤道转银道坐标 / equatorial to galactic coordinates.
//
//	ra: ICRS 赤经，单位度
//	dec: ICRS 赤纬，单位度
//
// 返回：
//
//	银经 Lon，单位度；银纬 Lat，单位度
//
// Returns galactic longitude and latitude in degrees from ICRS right ascension and declination.
func EquatorialToGalactic(ra, dec float64) Galactic {
	vector := sphericalToVector(ra, dec)
	rotated := matrixVectorMul(icrsToGalacticMatrix, vector)
	lon, lat := vectorToSpherical(rotated)
	return Galactic{Lon: lon, Lat: lat}
}

// GalacticToEquatorial 银道转赤道坐标 / galactic to equatorial coordinates.
//
//	lon: 银经，单位度
//	lat: 银纬，单位度
//
// 返回：
//
//	ICRS 赤经 RA，单位度；ICRS 赤纬 Dec，单位度
//
// Returns ICRS right ascension and declination in degrees from galactic longitude and latitude.
func GalacticToEquatorial(lon, lat float64) Equatorial {
	vector := sphericalToVector(lon, lat)
	rotated := matrixTransposeVectorMul(icrsToGalacticMatrix, vector)
	ra, dec := vectorToSpherical(rotated)
	return Equatorial{RA: ra, Dec: dec}
}

func sinCosDeg(angle float64) (sinValue, cosValue float64) {
	return math.Sincos(angle * math.Pi / 180)
}

func normalize360(angle float64) float64 {
	angle = math.Mod(angle, 360)
	if angle < 0 {
		angle += 360
	}
	return angle
}

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

func sphericalToVector(lon, lat float64) [3]float64 {
	sinLon, cosLon := sinCosDeg(lon)
	sinLat, cosLat := sinCosDeg(lat)
	return [3]float64{cosLat * cosLon, cosLat * sinLon, sinLat}
}

func vectorToSpherical(vector [3]float64) (lon, lat float64) {
	lon = normalize360(math.Atan2(vector[1], vector[0]) * 180 / math.Pi)
	lat = math.Asin(clampUnit(vector[2])) * 180 / math.Pi
	return lon, lat
}

func matrixVectorMul(matrix [3][3]float64, vector [3]float64) [3]float64 {
	return [3]float64{
		matrix[0][0]*vector[0] + matrix[0][1]*vector[1] + matrix[0][2]*vector[2],
		matrix[1][0]*vector[0] + matrix[1][1]*vector[1] + matrix[1][2]*vector[2],
		matrix[2][0]*vector[0] + matrix[2][1]*vector[1] + matrix[2][2]*vector[2],
	}
}

func matrixTransposeVectorMul(matrix [3][3]float64, vector [3]float64) [3]float64 {
	return [3]float64{
		matrix[0][0]*vector[0] + matrix[1][0]*vector[1] + matrix[2][0]*vector[2],
		matrix[0][1]*vector[0] + matrix[1][1]*vector[1] + matrix[2][1]*vector[2],
		matrix[0][2]*vector[0] + matrix[1][2]*vector[1] + matrix[2][2]*vector[2],
	}
}
