package eclipse

import (
	"math"
	"time"

	"b612.me/astro/basic"
)

const (
	sarosCycleLunations = 223
	sarosCycleDays      = float64(sarosCycleLunations) * solarEclipseSynodicMonthDays
	sarosWalkLimit      = 100

	sarosMagicYearOffset    = 3000
	sarosMagicCountMask     = 0x7f
	sarosMagicDayMask       = 0x1f
	sarosMagicMonthMask     = 0x0f
	sarosMagicYearMask      = 0x1fff
	sarosMagicCountShift    = 0
	sarosMagicDayShift      = 7
	sarosMagicMonthShift    = 12
	sarosMagicYearShift     = 16
	sarosMagicMatchLimitDay = 12.0
	sarosMagicTieEpsilonDay = 1e-9
)

// SarosInfo 沙罗序列信息, Saros series metadata.
type SarosInfo struct {
	// Series 是 NASA 沙罗系列编号；太阳食可能出现 0 号系列。
	// Series is the NASA Saros series number; solar eclipses may use series 0.
	Series int
	// Member 是本次食在该系列中的序号，从 1 开始计数。
	// Member is the 1-based index of this eclipse within the series.
	Member int
	// Count 是该系列的总成员数。
	// Count is the total number of eclipses in the series.
	Count int
}

type sarosMagic uint32

type sarosAnchor struct {
	Series int16
	Count  uint8
	Year   int16
	Month  uint8
	Day    uint8
}

type sarosHeadOverride struct {
	Series       int16
	Count        uint8
	HeadYear     int16
	HeadMonth    uint8
	HeadDay      uint8
	MemberOffset int8
}

var solarSarosHeadOverrides = [...]sarosHeadOverride{
	{Series: 22, Count: 71, HeadYear: -2192, HeadMonth: 5, HeadDay: 17, MemberOffset: -1},
}

var lunarSarosHeadOverrides = [...]sarosHeadOverride{
	{Series: 4, Count: 78, HeadYear: -2483, HeadMonth: 1, HeadDay: 12, MemberOffset: 2},
	{Series: 8, Count: 86, HeadYear: -2494, HeadMonth: 8, HeadDay: 7, MemberOffset: 0},
	{Series: 61, Count: 78, HeadYear: -762, HeadMonth: 12, HeadDay: 24, MemberOffset: 1},
}

func solarSarosInfo(ttJDE float64) (SarosInfo, bool) {
	if info, ok := matchSarosMagic(solarSarosAnchors[:], 0, solarSarosHeadOverrides[:], ttJDE); ok {
		return info, true
	}
	return solarSarosInfoByWalk(ttJDE)
}

func lunarSarosInfo(ttJDE float64) (SarosInfo, bool) {
	if info, ok := matchSarosMagic(lunarSarosAnchors[:], 1, lunarSarosHeadOverrides[:], ttJDE); ok {
		return info, true
	}
	return lunarSarosInfoByWalk(ttJDE)
}

func solarSarosInfoByWalk(ttJDE float64) (SarosInfo, bool) {
	headTT, member, ok := solarSarosHead(ttJDE)
	if !ok {
		return SarosInfo{}, false
	}
	if info, ok := matchSarosHeadOverride(solarSarosHeadOverrides[:], headTT, member); ok {
		return info, true
	}
	anchor, ok := matchSarosAnchor(solarSarosAnchors[:], 0, headTT)
	if !ok || member > int(anchor.Count) {
		return SarosInfo{}, false
	}
	return SarosInfo{
		Series: int(anchor.Series),
		Member: member,
		Count:  int(anchor.Count),
	}, true
}

func lunarSarosInfoByWalk(ttJDE float64) (SarosInfo, bool) {
	headTT, member, ok := lunarSarosHead(ttJDE)
	if !ok {
		return SarosInfo{}, false
	}
	if info, ok := matchSarosHeadOverride(lunarSarosHeadOverrides[:], headTT, member); ok {
		return info, true
	}
	anchor, ok := matchSarosAnchor(lunarSarosAnchors[:], 1, headTT)
	if !ok || member > int(anchor.Count) {
		return SarosInfo{}, false
	}
	return SarosInfo{
		Series: int(anchor.Series),
		Member: member,
		Count:  int(anchor.Count),
	}, true
}

func solarSarosHead(ttJDE float64) (float64, int, bool) {
	currentTT := ttJDE
	member := 1
	for step := 0; step < sarosWalkLimit; step++ {
		previousSeed := basic.CalcMoonSHByJDE(currentTT-sarosCycleDays, 0)
		previous := basic.SolarEclipseNASABulletinSplitK(previousSeed)
		if previous.Type == basic.SolarEclipseNone {
			return currentTT, member, true
		}
		currentTT = previous.GreatestEclipse
		member++
	}
	return 0, 0, false
}

func lunarSarosHead(ttJDE float64) (float64, int, bool) {
	currentTT := ttJDE
	member := 1
	for step := 0; step < sarosWalkLimit; step++ {
		previousSeed := basic.CalcMoonSHByJDE(currentTT-sarosCycleDays, 1)
		previous := basic.LunarEclipseDanjon(previousSeed)
		if previous.Type == basic.LunarEclipseNone {
			return currentTT, member, true
		}
		currentTT = previous.Maximum
		member++
	}
	return 0, 0, false
}

func matchSarosMagic(anchors []sarosMagic, seriesBase int, overrides []sarosHeadOverride, ttJDE float64) (SarosInfo, bool) {
	if info, ok := matchSarosMagicOverrides(overrides, ttJDE); ok {
		return info, true
	}
	bestDistance := math.Inf(1)
	best := SarosInfo{}
	for index, magic := range anchors {
		anchor := decodeSarosMagic(magic, seriesBase+index)
		info, distance, ok := matchSarosMagicCandidate(ttJDE, anchor, 0)
		if !ok {
			continue
		}
		if betterSarosMagicMatch(info, distance, best, bestDistance) {
			bestDistance = distance
			best = info
		}
	}
	if bestDistance <= sarosMagicMatchLimitDay {
		return best, true
	}
	return SarosInfo{}, false
}

func matchSarosMagicOverrides(overrides []sarosHeadOverride, ttJDE float64) (SarosInfo, bool) {
	bestDistance := math.Inf(1)
	best := SarosInfo{}
	for _, override := range overrides {
		anchor := sarosAnchor{
			Series: override.Series,
			Count:  override.Count,
			Year:   override.HeadYear,
			Month:  override.HeadMonth,
			Day:    override.HeadDay,
		}
		info, distance, ok := matchSarosMagicCandidate(ttJDE, anchor, int(override.MemberOffset))
		if !ok {
			continue
		}
		if betterSarosMagicMatch(info, distance, best, bestDistance) {
			bestDistance = distance
			best = info
		}
	}
	if bestDistance <= sarosMagicMatchLimitDay {
		return best, true
	}
	return SarosInfo{}, false
}

func matchSarosMagicCandidate(ttJDE float64, anchor sarosAnchor, memberOffset int) (SarosInfo, float64, bool) {
	headTT := basic.JDECalc(int(anchor.Year), int(anchor.Month), float64(anchor.Day))
	if math.IsNaN(headTT) {
		return SarosInfo{}, 0, false
	}
	member := int(math.Round((ttJDE-headTT)/sarosCycleDays)) + 1 + memberOffset
	if member < 1 || member > int(anchor.Count) {
		return SarosInfo{}, 0, false
	}
	expectedTT := headTT + float64(member-1-memberOffset)*sarosCycleDays
	return SarosInfo{
		Series: int(anchor.Series),
		Member: member,
		Count:  int(anchor.Count),
	}, math.Abs(ttJDE - expectedTT), true
}

func betterSarosMagicMatch(info SarosInfo, distance float64, best SarosInfo, bestDistance float64) bool {
	if distance < bestDistance-sarosMagicTieEpsilonDay {
		return true
	}
	if math.Abs(distance-bestDistance) > sarosMagicTieEpsilonDay {
		return false
	}
	if info.Series != best.Series {
		return info.Series < best.Series
	}
	return info.Member < best.Member
}

func decodeSarosMagic(magic sarosMagic, series int) sarosAnchor {
	value := uint32(magic)
	return sarosAnchor{
		Series: int16(series),
		Count:  uint8((value >> sarosMagicCountShift) & sarosMagicCountMask),
		Year:   int16(int((value>>sarosMagicYearShift)&sarosMagicYearMask) - sarosMagicYearOffset),
		Month:  uint8((value >> sarosMagicMonthShift) & sarosMagicMonthMask),
		Day:    uint8((value >> sarosMagicDayShift) & sarosMagicDayMask),
	}
}

func matchSarosAnchor(anchors []sarosMagic, seriesBase int, headTT float64) (sarosAnchor, bool) {
	headDate := basic.JDE2DateByZone(headTT, time.UTC, true)
	year, month, day := headDate.Date()
	monthNumber := int(month)
	for index, magic := range anchors {
		anchor := decodeSarosMagic(magic, seriesBase+index)
		if int(anchor.Year) == year && int(anchor.Month) == monthNumber && int(anchor.Day) == day {
			return anchor, true
		}
	}
	return sarosAnchor{}, false
}

func matchSarosHeadOverride(overrides []sarosHeadOverride, headTT float64, member int) (SarosInfo, bool) {
	headDate := basic.JDE2DateByZone(headTT, time.UTC, true)
	year, month, day := headDate.Date()
	monthNumber := int(month)
	for _, override := range overrides {
		if int(override.HeadYear) != year || int(override.HeadMonth) != monthNumber || int(override.HeadDay) != day {
			continue
		}
		adjustedMember := member + int(override.MemberOffset)
		if adjustedMember < 1 || adjustedMember > int(override.Count) {
			return SarosInfo{}, false
		}
		return SarosInfo{
			Series: int(override.Series),
			Member: adjustedMember,
			Count:  int(override.Count),
		}, true
	}
	return SarosInfo{}, false
}
