package sundial

import (
	"math"
	"time"

	"b612.me/astro/sun"
)

// TrueSolarTime 真太阳时 / apparent solar time.
//
// 返回 date 在经度 lon 处对应的真太阳时，口径沿用 `sun.ApparentSolarTime`。
// Returns the apparent solar time for date at longitude lon.
func TrueSolarTime(date time.Time, lon float64) time.Time {
	return sun.ApparentSolarTime(date, lon)
}

// MeanSolarTime 地方平太阳时 / local mean solar time.
//
// 返回 date 在经度 lon 处对应的地方平太阳时，结果时区为按经度换算的地方平太阳时区。
// 该实现直接按“真太阳时 - 均时差”构造，以与本仓库的真太阳时和均时差口径保持一致。
// Returns the local mean solar time for date at longitude lon. The result uses
// a synthetic local-mean-solar time zone derived from longitude and is built as
// apparent solar time minus equation of time to keep the package's conventions aligned.
func MeanSolarTime(date time.Time, lon float64) time.Time {
	return TrueSolarTime(date, lon).Add(time.Duration(-sun.EquationTime(date) * float64(time.Hour)))
}

// HourAngle 太阳时角 / solar hour angle.
//
// 返回经度 lon 处的有符号太阳时角，单位度。上午为负，下午为正，中午为 0°。
// Returns the signed apparent-solar hour angle at longitude lon, in degrees.
func HourAngle(date time.Time, lon float64) float64 {
	return normalizeSigned180(sun.HourAngle(date, lon, 0))
}

// MeanSolarHourAngle 平太阳时对应的太阳时角 / hour angle for local mean solar time.
//
// date 负责提供地方平太阳时日期与时区，原有钟面时间会被 meanSolarHours 替换；
// meanSolarHours 为地方平太阳时钟面读数，单位小时，例如 9.5 表示地方平太阳时 09:30。
// 返回对应的视太阳时角，单位度，上午为负，下午为正。
// date provides the local mean-solar date and timezone, while its clock fields are replaced by meanSolarHours.
// meanSolarHours is the local mean-solar clock reading in hours, for example 9.5 for 09:30.
// Returns the corresponding apparent-solar hour angle in degrees, negative in the morning and positive in the afternoon.
func MeanSolarHourAngle(date time.Time, meanSolarHours float64) float64 {
	if !isFinite(meanSolarHours) {
		return math.NaN()
	}
	sampleTime := dateWithClockHours(date, meanSolarHours)
	return normalizeSigned180(HourAngle(sampleTime, longitudeFromTimeZone(sampleTime)))
}

// ZoneTimeHourAngle 区时对应的太阳时角 / hour angle for zone time.
//
// zoneTimeHours 为 date 所在时区下的钟面读数，单位小时；lon 为当地经度，东正西负。
// 返回该区时在给定经度上对应的视太阳时角，单位度，上午为负，下午为正。
// date 提供民用日期和时区；其原有钟面时间会被 zoneTimeHours 替换。
// zoneTimeHours is the civil clock reading in the timezone carried by date, in hours; lon is east-positive longitude.
// Returns the apparent-solar hour angle for that civil time and longitude, in degrees, negative in the morning and positive in the afternoon.
// date provides the civil date and timezone, and its original clock fields are replaced by zoneTimeHours.
func ZoneTimeHourAngle(date time.Time, lon, zoneTimeHours float64) float64 {
	if !isFinite(zoneTimeHours) || !isFinite(lon) {
		return math.NaN()
	}
	return normalizeSigned180(HourAngle(dateWithClockHours(date, zoneTimeHours), lon))
}

// HorizontalHourLineAngle 水平日晷时线角 / horizontal sundial hour-line angle.
//
// lat 为纬度（北正南负），hourAngle 为有符号太阳时角，单位度。返回值是相对午线的时线角，
// 上午在东侧为负，下午在西侧为正。
// lat is the observer latitude in degrees and hourAngle is the signed solar
// hour angle in degrees. The result is the hour-line angle from the noon line:
// east/morning is negative and west/afternoon is positive.
func HorizontalHourLineAngle(lat, hourAngle float64) float64 {
	if !isFinite(hourAngle) || !isFinite(lat) {
		return math.NaN()
	}
	hourAngle = normalizeSigned180(hourAngle)
	latRad := lat * math.Pi / 180
	hourAngleRad := hourAngle * math.Pi / 180
	return math.Atan2(math.Sin(latRad)*math.Sin(hourAngleRad), math.Cos(hourAngleRad)) * 180 / math.Pi
}

// HorizontalHourLineAngleAt 水平日晷时线角 / horizontal sundial hour-line angle.
//
// 先按给定时刻和经度求瞬时视太阳时角，再结合纬度返回水平日晷的时线角。
// First derives the apparent-solar hour angle for the supplied instant and longitude, then returns the horizontal-dial hour-line angle for the given latitude.
func HorizontalHourLineAngleAt(date time.Time, lon, lat float64) float64 {
	return HorizontalHourLineAngle(lat, HourAngle(date, lon))
}

func dateWithClockHours(date time.Time, hours float64) time.Time {
	startOfDay := time.Date(date.Year(), date.Month(), date.Day(), 0, 0, 0, 0, date.Location())
	return startOfDay.Add(time.Duration(hours * float64(time.Hour)))
}

func longitudeFromTimeZone(date time.Time) float64 {
	_, offsetSeconds := date.Zone()
	return float64(offsetSeconds) / 240.0
}

func clockHours(date time.Time) float64 {
	return float64(date.Hour()) +
		float64(date.Minute())/60 +
		float64(date.Second())/3600 +
		float64(date.Nanosecond())/3.6e12
}

func normalizeSigned180(value float64) float64 {
	value = math.Mod(value, 360)
	if value < -180 {
		value += 360
	}
	if value >= 180 {
		value -= 360
	}
	return value
}

func isFinite(value float64) bool {
	return !math.IsNaN(value) && !math.IsInf(value, 0)
}
