package basic

import (
	"math"
	"sort"
)

const (
	starOccultationDiagramDefaultStepDays = 2.0 / 1440.0
	starOccultationDiagramMinStepDays     = 1.0 / 86400.0
	starOccultationDiagramMaxSamples      = 2000
	starOccultationDiagramDuplicateDays   = 1e-10
	starOccultationDiagramGeometryArcsec  = 0.05
	starOccultationDiagramPositionDeg     = 0.01
)

// StarOccultationDiagramOptions 控制本地恒星月掩图的轨迹采样。
// StarOccultationDiagramOptions controls local stellar-occultation diagram sampling.
type StarOccultationDiagramOptions struct {
	// StepDays 是请求的轨迹采样步长，单位为日；非正值或非有限值使用两分钟，正值小于一秒时使用一秒。长事件可能增大实际步长，使基础轨迹不超过 2000 个采样点；必要阶段帧仍会额外保留。结果会报告实际采用的值。
	// StepDays is the requested track sampling step in days. Non-positive or non-finite values use two minutes, and positive values below one second use one second. Long events may increase the effective step to keep the base track within 2000 samples; required phase frames are retained in addition. The result reports the effective value.
	StepDays float64
}

// StarOccultationDiagramFrame 描述一个时刻的站心月球与恒星几何。
// StarOccultationDiagramFrame describes topocentric Moon-star geometry at one instant.
type StarOccultationDiagramFrame struct {
	// JDE 是 TT 儒略历书日。
	// JDE is the TT Julian ephemeris day.
	JDE float64
	// StarXArcsec 和 StarYArcsec 是相对月心的切平面偏移，单位为角秒。X 向东为正，Y 向北为正。
	// StarXArcsec and StarYArcsec are tangent-plane offsets from the lunar center. X is positive east and Y is positive north.
	StarXArcsec float64
	StarYArcsec float64
	// MoonRadiusArcsec 是站心月球视半径，单位为角秒。
	// MoonRadiusArcsec is the topocentric apparent lunar semidiameter.
	MoonRadiusArcsec float64
	// SeparationArcsec 和 PositionAngleDeg 描述恒星相对月心的位置。
	// SeparationArcsec and PositionAngleDeg describe the star relative to the lunar center.
	SeparationArcsec float64
	PositionAngleDeg float64
	// MoonAltitudeDeg 和 MoonAzimuthDeg 是站心地平坐标。
	// MoonAltitudeDeg and MoonAzimuthDeg are topocentric horizontal coordinates.
	MoonAltitudeDeg float64
	MoonAzimuthDeg  float64
	// BehindMoon 表示点光源恒星位于月缘内侧。
	// BehindMoon is true while the point-source star lies strictly inside the lunar limb.
	BehindMoon bool
	// Label 是主阶段标识；Labels 在掠掩事件中保留重合阶段。
	// Label is the primary key phase; Labels retains coincident phases for grazing events.
	Label  string
	Labels []string
}

// StarOccultationDiagramResult 包含固定地点恒星月掩的几何数据。
// StarOccultationDiagramResult contains geometry for a fixed-site stellar occultation.
type StarOccultationDiagramResult struct {
	Occultation StarOccultationInfo
	Frames      []StarOccultationDiagramFrame
	// StepDays 是实际采用的基础轨迹采样步长，单位为日。
	// StepDays is the effective base-track sampling step in days.
	StepDays float64
}

type starOccultationDiagramTime struct {
	jde    float64
	labels []string
}

// StarOccultationDiagram 为已求解的固定地点恒星月掩计算以月心为原点的切平面轨迹。事件数据无效或不完整时，结果不含帧。
// StarOccultationDiagram computes a Moon-centered tangent-plane track for an already solved fixed-site stellar occultation. Invalid or incomplete event data produces a result without frames.
func StarOccultationDiagram(
	info StarOccultationInfo,
	star StarCoordinate,
	options StarOccultationDiagramOptions,
) StarOccultationDiagramResult {
	options = normalizeStarOccultationDiagramOptions(options)
	result := StarOccultationDiagramResult{Occultation: info, StepDays: options.StepDays}
	if star.Validate() != nil || info.Observer.Validate() != nil ||
		!info.ContactsComplete || info.Immersion.IsZero() || info.Greatest.IsZero() || info.Emersion.IsZero() ||
		info.Greatest.Before(info.Immersion) || info.Emersion.Before(info.Greatest) ||
		(info.Type != OccultationTotal && info.Type != OccultationGrazing) {
		return result
	}

	startTT := occultationTimeToTT(info.Immersion)
	greatestTT := occultationTimeToTT(info.Greatest)
	endTT := occultationTimeToTT(info.Emersion)
	immersionFrame, immersionOK := starOccultationDiagramFrameAt(startTT, star, info.Observer)
	greatestFrame, greatestOK := starOccultationDiagramFrameAt(greatestTT, star, info.Observer)
	emersionFrame, emersionOK := starOccultationDiagramFrameAt(endTT, star, info.Observer)
	if !immersionOK || !greatestOK || !emersionOK ||
		!starOccultationDiagramMatchesInfo(info, immersionFrame, greatestFrame, emersionFrame) {
		return result
	}
	times, stepDays := starOccultationDiagramTimes(startTT, greatestTT, endTT, options.StepDays)
	result.StepDays = stepDays
	result.Frames = make([]StarOccultationDiagramFrame, 0, len(times))
	for _, item := range times {
		frame, ok := starOccultationDiagramFrameAt(item.jde, star, info.Observer)
		if !ok {
			return StarOccultationDiagramResult{Occultation: info, StepDays: stepDays}
		}
		frame.Labels = append([]string(nil), item.labels...)
		frame.Label = starOccultationDiagramPrimaryLabel(item.labels)
		result.Frames = append(result.Frames, frame)
	}
	return result
}

func starOccultationDiagramMatchesInfo(
	info StarOccultationInfo,
	immersion, greatest, emersion StarOccultationDiagramFrame,
) bool {
	if !finite(info.MinimumSeparationArcsec) || !finite(info.MoonSemidiameterArcsec) ||
		!finite(info.PositionAngleDeg) {
		return false
	}
	if math.Abs(immersion.SeparationArcsec-immersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec ||
		math.Abs(emersion.SeparationArcsec-emersion.MoonRadiusArcsec) > starOccultationDiagramGeometryArcsec {
		return false
	}
	return math.Abs(greatest.SeparationArcsec-info.MinimumSeparationArcsec) <= starOccultationDiagramGeometryArcsec &&
		math.Abs(greatest.MoonRadiusArcsec-info.MoonSemidiameterArcsec) <= starOccultationDiagramGeometryArcsec &&
		math.Abs(signedAngleDifference(greatest.PositionAngleDeg, info.PositionAngleDeg)) <= starOccultationDiagramPositionDeg
}

func normalizeStarOccultationDiagramOptions(options StarOccultationDiagramOptions) StarOccultationDiagramOptions {
	if options.StepDays <= 0 || !finite(options.StepDays) {
		options.StepDays = starOccultationDiagramDefaultStepDays
	}
	if options.StepDays < starOccultationDiagramMinStepDays {
		options.StepDays = starOccultationDiagramMinStepDays
	}
	return options
}

func starOccultationDiagramTimes(startTT, greatestTT, endTT, stepDays float64) ([]starOccultationDiagramTime, float64) {
	if !finite(startTT) || !finite(greatestTT) || !finite(endTT) || greatestTT < startTT || endTT < greatestTT {
		return nil, stepDays
	}
	if endTT > startTT {
		if sampleCount := int(math.Ceil((endTT-startTT)/stepDays)) + 1; sampleCount > starOccultationDiagramMaxSamples {
			stepDays = (endTT - startTT) / float64(starOccultationDiagramMaxSamples-1)
		}
	}
	times := []starOccultationDiagramTime{
		{jde: startTT, labels: []string{"Immersion"}},
		{jde: greatestTT, labels: []string{"Greatest"}},
		{jde: endTT, labels: []string{"Emersion"}},
	}
	for jde := startTT + stepDays; jde < endTT; jde += stepDays {
		times = append(times, starOccultationDiagramTime{jde: jde})
	}
	sort.SliceStable(times, func(i, j int) bool {
		if times[i].jde == times[j].jde {
			return starOccultationDiagramLabelPriority(times[i].labels) < starOccultationDiagramLabelPriority(times[j].labels)
		}
		return times[i].jde < times[j].jde
	})
	return uniqueStarOccultationDiagramTimes(times), stepDays
}

func uniqueStarOccultationDiagramTimes(times []starOccultationDiagramTime) []starOccultationDiagramTime {
	unique := times[:0]
	for _, item := range times {
		if !finite(item.jde) {
			continue
		}
		if len(unique) == 0 || math.Abs(item.jde-unique[len(unique)-1].jde) > starOccultationDiagramDuplicateDays {
			item.labels = append([]string(nil), item.labels...)
			unique = append(unique, item)
			continue
		}
		unique[len(unique)-1].labels = mergeStarOccultationDiagramLabels(unique[len(unique)-1].labels, item.labels)
	}
	return unique
}

func mergeStarOccultationDiagramLabels(existing, incoming []string) []string {
	for _, label := range incoming {
		found := false
		for _, current := range existing {
			if current == label {
				found = true
				break
			}
		}
		if !found {
			existing = append(existing, label)
		}
	}
	return existing
}

func starOccultationDiagramPrimaryLabel(labels []string) string {
	for _, label := range labels {
		if label == "Greatest" {
			return label
		}
	}
	if len(labels) == 0 {
		return ""
	}
	return labels[0]
}

func starOccultationDiagramLabelPriority(labels []string) int {
	if len(labels) == 0 {
		return 99
	}
	switch labels[0] {
	case "Immersion":
		return 0
	case "Greatest":
		return 1
	case "Emersion":
		return 2
	default:
		return 99
	}
}

func starOccultationDiagramFrameAt(tt float64, star StarCoordinate, observer Observer) (StarOccultationDiagramFrame, bool) {
	position := starMoonPositionAt(tt, star, observer)
	moonRadius := moonTopocentricSemidiameterN(tt, observer, -1)
	if !position.valid || !finite(moonRadius) || moonRadius <= 0 {
		return StarOccultationDiagramFrame{}, false
	}
	separation := angularSeparationDegrees(position.moonRA, position.moonDec, position.starRA, position.starDec) * 3600
	positionAngle := occultationPositionAngle(position.moonRA, position.moonDec, position.starRA, position.starDec)
	if !finite(separation) || !finite(positionAngle) {
		return StarOccultationDiagramFrame{}, false
	}
	angle := positionAngle * math.Pi / 180
	return StarOccultationDiagramFrame{
		JDE:              tt,
		StarXArcsec:      separation * math.Sin(angle),
		StarYArcsec:      separation * math.Cos(angle),
		MoonRadiusArcsec: moonRadius,
		SeparationArcsec: separation,
		PositionAngleDeg: positionAngle,
		MoonAltitudeDeg:  occultationAltitude(tt, observer, position.moonRA, position.moonDec),
		MoonAzimuthDeg:   occultationAzimuth(tt, observer, position.moonRA, position.moonDec),
		BehindMoon:       separation < moonRadius-starOccultationGrazingTolerance,
	}, true
}
