package svg

import (
	"fmt"
	"html"
	"math"
	"strings"
	"time"

	"b612.me/astro/basic"
	eclipsecore "b612.me/astro/eclipse"
	"b612.me/astro/internal/svgasset"
)

const (
	lunarEclipseSVGDefaultWidth  = 960
	lunarEclipseSVGDefaultHeight = 620
	lunarEclipseSVGDefaultZone   = 8 * 60 * 60

	lunarEclipseSVGLanguageChinese = "zh"
	lunarEclipseSVGLanguageEnglish = "en"
)

// LunarEclipseSVGOptions 控制月食穿影 SVG 输出。
// LunarEclipseSVGOptions controls lunar eclipse shadow-path SVG output.
type LunarEclipseSVGOptions struct {
	// Width 和 Height 是 SVG 画布尺寸；<=0 时使用默认尺寸。
	// Width/Height are SVG canvas size; values <= 0 use defaults.
	Width  int
	Height int
	// Step 是月心路径采样步长；<=0 时使用 5 分钟。
	// Step is the Moon-center path sampling step; values <= 0 use five minutes.
	Step time.Duration
	// Title 是图题；为空时自动生成。
	// Title is the chart title; empty values use an automatic title.
	Title string
	// SummaryText 是标题下第一行摘要；为空时自动生成。
	// SummaryText is the first summary line below the title; empty values use an automatic summary.
	SummaryText string
	// MaximumText 是标题下第二行食甚说明；为空时自动生成。
	// MaximumText is the second line below the title for maximum-eclipse details; empty values use automatic text.
	MaximumText string
	// CoordinatesText 是标题下第三行坐标说明；为空时自动生成。
	// CoordinatesText is the third line below the title for coordinates; empty values use automatic text.
	CoordinatesText string
	// DurationText 是标题下第四行历时说明；为空时自动生成。
	// DurationText is the fourth line below the title for durations; empty values use automatic text.
	DurationText string
	// MetaText 是标题下第五行补充说明；为空时自动生成沙罗信息。
	// MetaText is the fifth line below the title; empty values use automatic Saros text.
	MetaText string
	// ContactsTitle 是接触时刻区标题；为空时自动生成。
	// ContactsTitle is the contacts-panel title; empty values use an automatic title.
	ContactsTitle string
	// DirectionText 是底部方向说明；为空时自动生成。
	// DirectionText is the footer direction note; empty values use an automatic note.
	DirectionText string
	// FooterNote 是底部补充说明；为空时自动生成。
	// FooterNote is the footer explanatory note; empty values use an automatic note.
	FooterNote string
	// Language 是标签语言；"en" 使用英文，其他值或空值使用中文。
	// Language controls label language; "en" uses English, other values or empty values use Chinese.
	Language string
	// Location 是图中显示时刻的时区；nil 时使用 UTC+8。
	// Location is the display timezone for chart times; nil uses UTC+8.
	Location *time.Location
}

type lunarEclipseSVGCalculator func(float64, basic.LunarEclipseDiagramOptions) basic.LunarEclipseDiagramResult
type lunarEclipseSVGFinder func(time.Time) LunarEclipseInfo

type lunarEclipseSVGLayout struct {
	width        float64
	height       float64
	cx           float64
	cy           float64
	scale        float64
	diagramLeft  float64
	diagramRight float64
	panelX       float64
	panelY       float64
}

type lunarEclipseSVGPoint struct {
	basic.LunarEclipseDiagramPoint
	X float64
	Y float64
}

type lunarEclipseSVGMaximumCoordinates struct {
	RA                float64
	Dec               float64
	EclipticLongitude float64
	EclipticLatitude  float64
	ConstellationCode string
	ConstellationName string
}

// LunarEclipseSVG 生成月食穿影图 SVG，默认使用 Danjon 影半径模型。
// LunarEclipseSVG generates an SVG lunar eclipse shadow-path chart, using the Danjon shadow model by default.
func LunarEclipseSVG(date time.Time, options LunarEclipseSVGOptions) (string, bool) {
	return LunarEclipseSVGDanjon(date, options)
}

// LunarEclipseSVGDanjon 生成月食穿影图 SVG，使用 Danjon 影半径模型。
// LunarEclipseSVGDanjon generates an SVG lunar eclipse shadow-path chart with the Danjon shadow model.
func LunarEclipseSVGDanjon(date time.Time, options LunarEclipseSVGOptions) (string, bool) {
	return lunarEclipseSVG(date, options, basic.LunarEclipseDiagramDanjon, eclipsecore.ClosestLunarEclipseDanjon)
}

// LunarEclipseSVGChauvenet 生成月食穿影图 SVG，使用 Chauvenet 影半径模型。
// LunarEclipseSVGChauvenet generates an SVG lunar eclipse shadow-path chart with the Chauvenet shadow model.
func LunarEclipseSVGChauvenet(date time.Time, options LunarEclipseSVGOptions) (string, bool) {
	return lunarEclipseSVG(date, options, basic.LunarEclipseDiagramChauvenet, eclipsecore.ClosestLunarEclipseChauvenet)
}

func lunarEclipseSVG(
	date time.Time,
	options LunarEclipseSVGOptions,
	calculator lunarEclipseSVGCalculator,
	finder lunarEclipseSVGFinder,
) (string, bool) {
	options = normalizeLunarEclipseSVGOptions(options)
	diagram := calculator(timeToTTJDE(date), basic.LunarEclipseDiagramOptions{
		StepDays: durationToDays(options.Step),
	})
	if diagram.Eclipse.Type == basic.LunarEclipseNone || len(diagram.Points) == 0 {
		return "", false
	}
	info := lunarEclipseInfoFromBasic(diagram.Eclipse, options.Location)
	if finder != nil {
		coreInfo := finder(info.Maximum)
		info.HasSaros = coreInfo.HasSaros
		info.Saros = coreInfo.Saros
	}
	return renderLunarEclipseSVG(info, diagram, options), true
}

func normalizeLunarEclipseSVGOptions(options LunarEclipseSVGOptions) LunarEclipseSVGOptions {
	if options.Width <= 0 {
		options.Width = lunarEclipseSVGDefaultWidth
	}
	if options.Height <= 0 {
		options.Height = lunarEclipseSVGDefaultHeight
	}
	if options.Location == nil {
		options.Location = time.FixedZone("UTC+8", lunarEclipseSVGDefaultZone)
	}
	if strings.EqualFold(options.Language, lunarEclipseSVGLanguageEnglish) {
		options.Language = lunarEclipseSVGLanguageEnglish
	} else {
		options.Language = lunarEclipseSVGLanguageChinese
	}
	return options
}

func renderLunarEclipseSVG(
	info LunarEclipseInfo,
	diagram basic.LunarEclipseDiagramResult,
	options LunarEclipseSVGOptions,
) string {
	headerTexts := lunarEclipseSVGHeaderTexts(info, options)
	layout := lunarEclipseSVGLayoutFor(diagram, options, lunarEclipseSVGHeaderBottom(headerTexts))
	points := lunarEclipseSVGPoints(diagram.Points)
	mapX := func(x float64) float64 { return layout.cx - x*layout.scale }
	mapY := func(y float64) float64 { return layout.cy - y*layout.scale }
	title := lunarEclipseSVGTitleText(info, options)

	var b strings.Builder
	fmt.Fprintf(&b, `<svg xmlns="http://www.w3.org/2000/svg" width="%d" height="%d" viewBox="0 0 %d %d">`, options.Width, options.Height, options.Width, options.Height)
	b.WriteString(`<defs>`)
	b.WriteString(svgasset.MoonFaceSymbol())
	b.WriteString(`</defs>`)
	b.WriteString(`<rect width="100%" height="100%" fill="#efefed"/>`)
	fmt.Fprintf(&b, `<rect x="22" y="18" width="%.3f" height="%.3f" fill="#ffffff" stroke="#c9c9c6" stroke-width="1.2"/>`,
		layout.width-44, layout.height-36)
	fmt.Fprintf(&b, `<text x="%.3f" y="44" fill="#111111" font-family="Georgia, 'Times New Roman', serif" font-size="26" font-weight="700" text-anchor="middle">%s</text>`,
		layout.width/2, html.EscapeString(title))
	fmt.Fprintf(&b, `<line x1="%.3f" y1="57" x2="%.3f" y2="57" stroke="#555" stroke-width="1"/>`, layout.width/2-78, layout.width/2+78)
	writeLunarEclipseSummary(&b, headerTexts, options)
	fmt.Fprintf(&b, `<circle cx="%.3f" cy="%.3f" r="%.3f" fill="#e9e9e9" stroke="#d6d6d6" stroke-width="1.2"/>`,
		layout.cx, layout.cy, diagram.PenumbraRadius*layout.scale)
	fmt.Fprintf(&b, `<circle cx="%.3f" cy="%.3f" r="%.3f" fill="#b21f16" stroke="#83170f" stroke-width="1.3"/>`,
		layout.cx, layout.cy, diagram.UmbraRadius*layout.scale)
	writeLunarEclipseShadowLabels(&b, layout, diagram, options.Language)
	writeLunarEclipseAxes(&b, layout.cx, layout.cy, diagram.PenumbraRadius*layout.scale, options.Language)
	writeLunarEclipseEclipticLine(&b, layout, diagram, mapX, mapY, options.Language)

	if len(points) > 0 {
		b.WriteString(`<path d="`)
		for i, point := range points {
			if i == 0 {
				fmt.Fprintf(&b, `M %.3f %.3f`, mapX(point.X), mapY(point.Y))
				continue
			}
			fmt.Fprintf(&b, ` L %.3f %.3f`, mapX(point.X), mapY(point.Y))
		}
		b.WriteString(`" fill="none" stroke="#333333" stroke-width="1.1" stroke-dasharray="5 4" stroke-linecap="round" stroke-linejoin="round"/>`)
	}

	eventPoints := lunarEclipseSVGEventPoints(points)
	for _, point := range eventPoints {
		if point.Label == "Greatest" {
			continue
		}
		writeLunarEclipseMoon(&b, point, diagram, mapX(point.X), mapY(point.Y), layout.scale, false)
	}
	for _, point := range eventPoints {
		if point.Label == "Greatest" {
			writeLunarEclipseMoon(&b, point, diagram, mapX(point.X), mapY(point.Y), layout.scale, true)
			break
		}
	}
	for _, point := range eventPoints {
		if point.Label == "" {
			continue
		}
		x := mapX(point.X)
		y := mapY(point.Y)
		writeLunarEclipseEventLabel(&b, point.Label, x, y, layout.cx, diagram.MoonRadius*layout.scale, options.Language)
	}

	writeLunarEclipseContacts(&b, info, options, layout.panelX, layout.panelY)
	writeLunarEclipseFooter(&b, info, options, layout)

	b.WriteString(`</svg>`)
	return b.String()
}

func lunarEclipseSVGLayoutFor(
	diagram basic.LunarEclipseDiagramResult,
	options LunarEclipseSVGOptions,
	headerBottom float64,
) lunarEclipseSVGLayout {
	width := float64(options.Width)
	height := float64(options.Height)
	margin := math.Max(32, math.Min(48, width*0.05))
	panelWidth := math.Max(210, math.Min(260, width*0.24))
	diagramLeft := margin
	diagramRight := width - panelWidth - 34
	if diagramRight-diagramLeft < width*0.48 {
		diagramRight = width - margin
	}
	topReserved := math.Max(166, headerBottom+24)
	bottomReserved := 82.0
	extent := diagram.PenumbraRadius + diagram.MoonRadius + 0.72
	scale := math.Min((diagramRight-diagramLeft)/(2*extent), (height-topReserved-bottomReserved)/(2*extent))
	if scale <= 0 || math.IsNaN(scale) || math.IsInf(scale, 0) {
		scale = 1
	}
	cx := (diagramLeft + diagramRight) / 2
	cy := topReserved + (height-topReserved-bottomReserved)/2 + 12
	panelX := diagramRight + 22
	if panelX+panelWidth > width-margin/2 {
		panelX = width - panelWidth - margin/2
	}
	if panelX < diagramRight {
		panelX = diagramRight
	}
	return lunarEclipseSVGLayout{
		width:        width,
		height:       height,
		cx:           cx,
		cy:           cy,
		scale:        scale,
		diagramLeft:  diagramLeft,
		diagramRight: diagramRight,
		panelX:       panelX,
		panelY:       math.Max(148, cy-88),
	}
}

func lunarEclipseSVGTitle(info LunarEclipseInfo, language string) string {
	return fmt.Sprintf("%s %s", info.Maximum.Format("2006-01-02"), lunarEclipseSVGTypeName(info.Type, language))
}

func lunarEclipseSVGTitleText(info LunarEclipseInfo, options LunarEclipseSVGOptions) string {
	if options.Title != "" {
		return options.Title
	}
	return lunarEclipseSVGTitle(info, options.Language)
}

func lunarEclipseSVGHeaderTexts(info LunarEclipseInfo, options LunarEclipseSVGOptions) []string {
	lines := []string{
		lunarEclipseSVGSummaryText(info, options),
		lunarEclipseSVGMaximumTextValue(info, options),
		lunarEclipseSVGCoordinatesTextValue(info, options),
		lunarEclipseSVGDurationTextValue(info, options),
		lunarEclipseSVGMetaTextValue(info, options),
	}
	filtered := make([]string, 0, len(lines))
	for _, line := range lines {
		if line != "" {
			filtered = append(filtered, line)
		}
	}
	return filtered
}

func lunarEclipseSVGSummaryText(info LunarEclipseInfo, options LunarEclipseSVGOptions) string {
	if options.SummaryText != "" {
		return options.SummaryText
	}
	return lunarEclipseSVGSummary(info, options.Language)
}

func lunarEclipseSVGMaximumTextValue(info LunarEclipseInfo, options LunarEclipseSVGOptions) string {
	if options.MaximumText != "" {
		return options.MaximumText
	}
	return lunarEclipseSVGMaximumText(info, options)
}

func lunarEclipseSVGCoordinatesTextValue(info LunarEclipseInfo, options LunarEclipseSVGOptions) string {
	if options.CoordinatesText != "" {
		return options.CoordinatesText
	}
	coordinates := lunarEclipseSVGMaximumCoordinatesFor(info, options.Language)
	return lunarEclipseSVGMaximumCoordinatesText(coordinates, options.Language)
}

func lunarEclipseSVGDurationTextValue(info LunarEclipseInfo, options LunarEclipseSVGOptions) string {
	if options.DurationText != "" {
		return options.DurationText
	}
	return lunarEclipseSVGDurationSummary(info, options.Language)
}

func lunarEclipseSVGMetaTextValue(info LunarEclipseInfo, options LunarEclipseSVGOptions) string {
	if options.MetaText != "" {
		return options.MetaText
	}
	return lunarEclipseSVGMetaText(info, options.Language)
}

func lunarEclipseSVGContactsTitleText(options LunarEclipseSVGOptions) string {
	if options.ContactsTitle != "" {
		return options.ContactsTitle
	}
	if options.Language == lunarEclipseSVGLanguageEnglish {
		return "Contacts"
	}
	return "接触时刻"
}

func lunarEclipseSVGDirectionTextValue(options LunarEclipseSVGOptions) string {
	if options.DirectionText != "" {
		return options.DirectionText
	}
	return lunarEclipseSVGDirectionText(options.Language)
}

func lunarEclipseSVGFooterNoteText(options LunarEclipseSVGOptions) string {
	if options.FooterNote != "" {
		return options.FooterNote
	}
	note := "图中月面大小和影半径均按实际相对角半径缩放。"
	if options.Language == lunarEclipseSVGLanguageEnglish {
		note = "Moon disks and shadow radii are drawn to the same relative angular-radius scale."
	}
	return note
}

func lunarEclipseSVGHeaderLineY(index int) float64 {
	return 84 + float64(index)*22
}

func lunarEclipseSVGHeaderBottom(lines []string) float64 {
	if len(lines) == 0 {
		return 72
	}
	return lunarEclipseSVGHeaderLineY(len(lines)-1) + 14
}

func lunarEclipseSVGSummary(info LunarEclipseInfo, language string) string {
	if language == lunarEclipseSVGLanguageEnglish {
		return fmt.Sprintf("type=%s  penumbral=%.4f  umbral=%.4f",
			lunarEclipseSVGTypeName(info.Type, language), info.PenumbralMagnitude, info.UmbralMagnitude)
	}
	return fmt.Sprintf("食型=%s  半影食分=%.4f  本影食分=%.4f",
		lunarEclipseSVGTypeName(info.Type, language), info.PenumbralMagnitude, info.UmbralMagnitude)
}

func writeLunarEclipseSummary(b *strings.Builder, lines []string, options LunarEclipseSVGOptions) {
	for index, line := range lines {
		fontSize := 13
		fill := "#333"
		if index == 0 {
			fontSize = 14
			fill = "#222"
		}
		fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="%s" font-family="Georgia, 'Times New Roman', serif" font-size="%d" text-anchor="middle">%s</text>`,
			float64(options.Width)/2, lunarEclipseSVGHeaderLineY(index), fill, fontSize, html.EscapeString(line))
	}
}

func lunarEclipseSVGDirectionText(language string) string {
	if language == lunarEclipseSVGLanguageEnglish {
		return "North is up and east is left; the ecliptic is projected near greatest eclipse."
	}
	return "上北下南，左东右西；黄道按食甚附近天球投影绘制。"
}

func lunarEclipseSVGMaximumText(info LunarEclipseInfo, options LunarEclipseSVGOptions) string {
	maximum := info.Maximum.In(options.Location).Format("2006-01-02 15:04:05 MST")
	if options.Language == lunarEclipseSVGLanguageEnglish {
		return "Maximum: " + maximum
	}
	return "食甚：" + maximum
}

func lunarEclipseSVGMaximumCoordinatesFor(
	info LunarEclipseInfo,
	language string,
) lunarEclipseSVGMaximumCoordinates {
	jde := timeToTTJDE(info.Maximum)
	ra, dec := basic.HMoonTrueRaDec(jde)
	lon := basic.HMoonApparentLo(jde)
	lat := basic.HMoonTrueBo(jde)
	code := basic.ConstellationCode(ra, dec, jde)
	name := basic.ConstellationNameByCodeZH(code)
	if language == lunarEclipseSVGLanguageEnglish {
		name = basic.ConstellationNameByCodeEN(code)
	}
	return lunarEclipseSVGMaximumCoordinates{
		RA:                ra,
		Dec:               dec,
		EclipticLongitude: lon,
		EclipticLatitude:  lat,
		ConstellationCode: code,
		ConstellationName: name,
	}
}

func lunarEclipseSVGMaximumCoordinatesText(
	coordinates lunarEclipseSVGMaximumCoordinates,
	language string,
) string {
	if language == lunarEclipseSVGLanguageEnglish {
		return fmt.Sprintf("Moon: RA %s  Dec %s  ecl.lon %.4f deg  ecl.lat %.4f deg  %s",
			lunarEclipseSVGFormatRA(coordinates.RA),
			lunarEclipseSVGFormatSignedAngle(coordinates.Dec),
			coordinates.EclipticLongitude,
			coordinates.EclipticLatitude,
			coordinates.ConstellationName,
		)
	}
	return fmt.Sprintf("月球：赤经 %s  赤纬 %s  黄经 %.4f°  黄纬 %.4f°  %s",
		lunarEclipseSVGFormatRA(coordinates.RA),
		lunarEclipseSVGFormatSignedAngle(coordinates.Dec),
		coordinates.EclipticLongitude,
		coordinates.EclipticLatitude,
		coordinates.ConstellationName,
	)
}

func lunarEclipseSVGFormatRA(degree float64) string {
	totalSeconds := int(math.Round(normalizeDegree360(degree) / 15 * 3600))
	totalSeconds %= 24 * 3600
	hours := totalSeconds / 3600
	minutes := totalSeconds % 3600 / 60
	seconds := totalSeconds % 60
	return fmt.Sprintf("%02dh%02dm%02ds", hours, minutes, seconds)
}

func lunarEclipseSVGFormatSignedAngle(degree float64) string {
	sign := "+"
	if degree < 0 {
		sign = "-"
		degree = -degree
	}
	totalSeconds := int(math.Round(degree * 3600))
	degrees := totalSeconds / 3600
	minutes := totalSeconds % 3600 / 60
	seconds := totalSeconds % 60
	return fmt.Sprintf("%s%02d°%02d′%02d″", sign, degrees, minutes, seconds)
}

func lunarEclipseSVGDurationSummary(info LunarEclipseInfo, language string) string {
	penumbral := lunarEclipseSVGFormatDuration(info.PenumbralEnd.Sub(info.PenumbralStart))
	if language == lunarEclipseSVGLanguageEnglish {
		parts := []string{"Penumbral duration " + penumbral}
		if info.HasPartial {
			parts = append(parts, "Umbral duration "+lunarEclipseSVGFormatDuration(info.PartialEnd.Sub(info.PartialStart)))
		}
		if info.HasTotal {
			parts = append(parts, "Total duration "+lunarEclipseSVGFormatDuration(info.TotalEnd.Sub(info.TotalStart)))
		}
		return strings.Join(parts, "   ")
	}
	parts := []string{"半影历时 " + penumbral}
	if info.HasPartial {
		parts = append(parts, "本影历时 "+lunarEclipseSVGFormatDuration(info.PartialEnd.Sub(info.PartialStart)))
	}
	if info.HasTotal {
		parts = append(parts, "全食历时 "+lunarEclipseSVGFormatDuration(info.TotalEnd.Sub(info.TotalStart)))
	}
	return strings.Join(parts, "   ")
}

func lunarEclipseSVGMetaText(info LunarEclipseInfo, language string) string {
	if !info.HasSaros {
		return ""
	}
	if language == lunarEclipseSVGLanguageEnglish {
		return fmt.Sprintf("Lunar Saros %d  %d/%d", info.Saros.Series, info.Saros.Member, info.Saros.Count)
	}
	return fmt.Sprintf("沙罗 %d  第 %d/%d 个成员", info.Saros.Series, info.Saros.Member, info.Saros.Count)
}

func lunarEclipseSVGFormatDuration(duration time.Duration) string {
	if duration < 0 {
		duration = -duration
	}
	totalSeconds := int(duration.Round(time.Second).Seconds())
	hours := totalSeconds / 3600
	minutes := totalSeconds % 3600 / 60
	seconds := totalSeconds % 60
	return fmt.Sprintf("%02d:%02d:%02d", hours, minutes, seconds)
}

func lunarEclipseSVGTypeName(eclipseType LunarEclipseType, language string) string {
	if language == lunarEclipseSVGLanguageEnglish {
		switch eclipseType {
		case LunarEclipsePenumbral:
			return "Penumbral Lunar Eclipse"
		case LunarEclipsePartial:
			return "Partial Lunar Eclipse"
		case LunarEclipseTotal:
			return "Total Lunar Eclipse"
		default:
			return "No Lunar Eclipse"
		}
	}
	switch eclipseType {
	case LunarEclipsePenumbral:
		return "半影月食"
	case LunarEclipsePartial:
		return "月偏食"
	case LunarEclipseTotal:
		return "月全食"
	default:
		return "无月食"
	}
}

func writeLunarEclipseAxes(b *strings.Builder, cx, cy, radius float64, language string) {
	north, east, west, south := "北", "东", "西", "南"
	if language == lunarEclipseSVGLanguageEnglish {
		north, east, west, south = "N", "E", "W", "S"
	}
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111" font-family="Georgia, 'Times New Roman', serif" font-size="14" font-weight="700" text-anchor="middle">%s</text>`,
		cx, cy-radius-18, html.EscapeString(north))
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111" font-family="Georgia, 'Times New Roman', serif" font-size="14" font-weight="700" text-anchor="middle">%s</text>`,
		cx-radius-22, cy+4, html.EscapeString(east))
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111" font-family="Georgia, 'Times New Roman', serif" font-size="14" font-weight="700" text-anchor="middle">%s</text>`,
		cx+radius+22, cy+4, html.EscapeString(west))
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111" font-family="Georgia, 'Times New Roman', serif" font-size="14" font-weight="700" text-anchor="middle">%s</text>`,
		cx, cy+radius+28, html.EscapeString(south))
}

func lunarEclipseSVGPoints(points []basic.LunarEclipseDiagramPoint) []lunarEclipseSVGPoint {
	result := make([]lunarEclipseSVGPoint, 0, len(points))
	for _, point := range points {
		distance := math.Hypot(point.X, point.Y)
		positionAngleRad := lunarEclipseMoonCenterPositionAngle(point.JDE) * math.Pi / 180
		result = append(result, lunarEclipseSVGPoint{
			LunarEclipseDiagramPoint: point,
			X:                        distance * math.Sin(positionAngleRad),
			Y:                        distance * math.Cos(positionAngleRad),
		})
	}
	return result
}

func writeLunarEclipseEclipticLine(
	b *strings.Builder,
	layout lunarEclipseSVGLayout,
	diagram basic.LunarEclipseDiagramResult,
	mapX, mapY func(float64) float64,
	language string,
) {
	unitX, unitY, ok := lunarEclipseSVGEclipticDirection(diagram.Eclipse.Maximum)
	if !ok {
		return
	}

	extent := (diagram.PenumbraRadius + diagram.MoonRadius) * 1.42
	screenStartX := mapX(-unitX * extent)
	screenStartY := mapY(-unitY * extent)
	screenEndX := mapX(unitX * extent)
	screenEndY := mapY(unitY * extent)
	if math.IsNaN(screenStartX) || math.IsNaN(screenStartY) || math.IsNaN(screenEndX) || math.IsNaN(screenEndY) {
		return
	}

	fmt.Fprintf(b, `<line x1="%.3f" y1="%.3f" x2="%.3f" y2="%.3f" stroke="#555" stroke-width="1" stroke-dasharray="4 3" opacity="0.82"/>`,
		screenStartX, screenStartY, screenEndX, screenEndY)

	labelX := screenStartX
	labelY := screenStartY
	anchor := "end"
	if screenEndX < screenStartX {
		labelX = screenEndX
		labelY = screenEndY
	}
	labelX -= 8
	if labelX < layout.diagramLeft+18 {
		labelX += 16
		anchor = "start"
	}
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#444" font-family="Georgia, 'Times New Roman', serif" font-size="12" text-anchor="%s">%s</text>`,
		labelX, labelY+4, anchor, html.EscapeString(lunarEclipseSVGLabelEcliptic(language)))
}

func lunarEclipseSVGEclipticDirection(ttJDE float64) (float64, float64, bool) {
	originRA, originDec := lunarEclipseShadowCenterRaDec(ttJDE)
	centerLongitude := normalizeDegree360(basic.HSunApparentLo(ttJDE) + 180)
	ra1, dec1 := basic.LoBoToRaDec(ttJDE, centerLongitude-1, 0)
	ra2, dec2 := basic.LoBoToRaDec(ttJDE, centerLongitude+1, 0)
	x1, y1 := lunarEclipseSVGTangentOffset(originRA, originDec, ra1, dec1)
	x2, y2 := lunarEclipseSVGTangentOffset(originRA, originDec, ra2, dec2)
	dx := x2 - x1
	dy := y2 - y1
	length := math.Hypot(dx, dy)
	if length == 0 || math.IsNaN(length) || math.IsInf(length, 0) {
		return 0, 0, false
	}
	return dx / length, dy / length, true
}

func lunarEclipseSVGTangentOffset(originRA, originDec, targetRA, targetDec float64) (float64, float64) {
	separation := lunarEclipseSVGAngularSeparation(originRA, originDec, targetRA, targetDec)
	positionAngleRad := lunarEclipsePositionAngle(originRA, originDec, targetRA, targetDec) * math.Pi / 180
	return separation * math.Sin(positionAngleRad), separation * math.Cos(positionAngleRad)
}

func lunarEclipseSVGAngularSeparation(ra1, dec1, ra2, dec2 float64) float64 {
	ra1Rad := ra1 * math.Pi / 180
	dec1Rad := dec1 * math.Pi / 180
	ra2Rad := ra2 * math.Pi / 180
	dec2Rad := dec2 * math.Pi / 180
	cosDistance := math.Sin(dec1Rad)*math.Sin(dec2Rad) +
		math.Cos(dec1Rad)*math.Cos(dec2Rad)*math.Cos(ra2Rad-ra1Rad)
	if cosDistance > 1 {
		cosDistance = 1
	}
	if cosDistance < -1 {
		cosDistance = -1
	}
	return math.Acos(cosDistance) * 180 / math.Pi
}

func lunarEclipseSVGLabelEcliptic(language string) string {
	if language == lunarEclipseSVGLanguageEnglish {
		return "Ecliptic"
	}
	return "黄道"
}

func writeLunarEclipseShadowLabels(
	b *strings.Builder,
	layout lunarEclipseSVGLayout,
	diagram basic.LunarEclipseDiagramResult,
	language string,
) {
	penumbraLabel, umbraLabel := "地球半影", "地球本影"
	if language == lunarEclipseSVGLanguageEnglish {
		penumbraLabel, umbraLabel = "Earth's Penumbra", "Earth's Umbra"
	}
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#333" font-family="Georgia, 'Times New Roman', serif" font-size="13" font-weight="700" text-anchor="middle">%s</text>`,
		layout.cx, layout.cy-diagram.PenumbraRadius*layout.scale+28, html.EscapeString(penumbraLabel))
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111" font-family="Georgia, 'Times New Roman', serif" font-size="13" font-weight="700" text-anchor="middle">%s</text>`,
		layout.cx, layout.cy-diagram.UmbraRadius*layout.scale+22, html.EscapeString(umbraLabel))
}

func lunarEclipseSVGEventPoints(points []lunarEclipseSVGPoint) []lunarEclipseSVGPoint {
	events := make([]lunarEclipseSVGPoint, 0, 7)
	for _, point := range points {
		for _, label := range lunarEclipseSVGPointLabels(point) {
			event := point
			event.Label = label
			event.Labels = []string{label}
			events = append(events, event)
		}
	}
	return events
}

func lunarEclipseSVGPointLabels(point lunarEclipseSVGPoint) []string {
	if len(point.Labels) > 0 {
		return point.Labels
	}
	if point.Label == "" {
		return nil
	}
	return []string{point.Label}
}

func writeLunarEclipseMoon(
	b *strings.Builder,
	point lunarEclipseSVGPoint,
	diagram basic.LunarEclipseDiagramResult,
	x, y, scale float64,
	greatest bool,
) {
	radius := diagram.MoonRadius * scale
	opacity := 0.64
	if greatest {
		opacity = 0.9
	}
	fmt.Fprintf(b, `<g class="event-moon">`)
	fmt.Fprintf(b, `<use href="#le-moon" x="%.3f" y="%.3f" width="%.3f" height="%.3f" opacity="%.2f"/>`,
		x-radius, y-radius, radius*2, radius*2, opacity)
	if lunarEclipseSVGDeepUmbra(point.LunarEclipseDiagramPoint, diagram) {
		tintOpacity := 0.46
		if greatest {
			tintOpacity = 0.58
		}
		fmt.Fprintf(b, `<circle cx="%.3f" cy="%.3f" r="%.3f" fill="#d66f1f" opacity="%.2f"/>`,
			x, y, radius, tintOpacity)
	}
	fmt.Fprintf(b, `<circle cx="%.3f" cy="%.3f" r="%.3f" fill="none" stroke="#b9b9b9" stroke-width="0.8" opacity="0.9"/>`, x, y, radius)
	b.WriteString(`</g>`)
}

func lunarEclipseSVGDeepUmbra(point basic.LunarEclipseDiagramPoint, diagram basic.LunarEclipseDiagramResult) bool {
	return math.Hypot(point.X, point.Y) < diagram.UmbraRadius+diagram.MoonRadius*0.75
}

func writeLunarEclipseEventLabel(
	b *strings.Builder,
	label string,
	x, y, cx, moonRadius float64,
	language string,
) {
	text := lunarEclipseSVGEventName(label, language)
	dx := moonRadius*0.72 + 6
	anchor := "start"
	if x < cx {
		dx = -dx
		anchor = "end"
	}
	dy := -moonRadius*0.35 - 4
	if label == "Greatest" {
		dy = moonRadius + 15
		anchor = "middle"
		dx = 0
	}
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#2554c7" font-family="Georgia, 'Times New Roman', serif" font-size="12" text-anchor="%s">%s</text>`,
		x+dx, y+dy, anchor, html.EscapeString(text))
}

func lunarEclipseSVGEventName(label, language string) string {
	if language == lunarEclipseSVGLanguageEnglish {
		switch label {
		case "Greatest":
			return "Greatest"
		default:
			return label
		}
	}
	switch label {
	case "P1":
		return "P1 半影始"
	case "U1":
		return "U1 初亏"
	case "U2":
		return "U2 食既"
	case "Greatest":
		return "食甚"
	case "U3":
		return "U3 生光"
	case "U4":
		return "U4 复圆"
	case "P4":
		return "P4 半影终"
	default:
		return label
	}
}

type lunarEclipseSVGContact struct {
	label    string
	name     string
	time     time.Time
	angle    float64
	hasAngle bool
}

func writeLunarEclipseContacts(
	b *strings.Builder,
	info LunarEclipseInfo,
	options LunarEclipseSVGOptions,
	x, y float64,
) {
	contacts := lunarEclipseSVGContacts(info, options.Language)
	if len(contacts) == 0 {
		return
	}
	title := lunarEclipseSVGContactsTitleText(options)
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#111" font-family="Georgia, 'Times New Roman', serif" font-size="13" font-weight="700">%s (%s)</text>`,
		x, y, html.EscapeString(title), html.EscapeString(options.Location.String()))
	for index, contact := range contacts {
		line := fmt.Sprintf("%s %s  %s", contact.label, contact.name, contact.time.In(options.Location).Format("15:04:05"))
		if contact.hasAngle {
			if options.Language == lunarEclipseSVGLanguageEnglish {
				line = fmt.Sprintf("%s  PA %.1f°", line, contact.angle)
			} else {
				line = fmt.Sprintf("%s  方位 %.1f°", line, contact.angle)
			}
		}
		fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#222" font-family="Georgia, 'Times New Roman', serif" font-size="12">%s</text>`,
			x, y+float64(index+1)*18, html.EscapeString(line))
	}
}

func writeLunarEclipseFooter(
	b *strings.Builder,
	info LunarEclipseInfo,
	options LunarEclipseSVGOptions,
	layout lunarEclipseSVGLayout,
) {
	_ = info
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#333" font-family="Georgia, 'Times New Roman', serif" font-size="12">%s</text>`,
		40.0, layout.height-54, html.EscapeString(lunarEclipseSVGDirectionTextValue(options)))
	note := lunarEclipseSVGFooterNoteText(options)
	fmt.Fprintf(b, `<text x="%.3f" y="%.3f" fill="#555" font-family="Georgia, 'Times New Roman', serif" font-size="12">%s</text>`,
		40.0, layout.height-34, html.EscapeString(note))
}

func lunarEclipseSVGContacts(info LunarEclipseInfo, language string) []lunarEclipseSVGContact {
	angles := lunarEclipseContactAngleMap(info.ContactPoints)
	contacts := []lunarEclipseSVGContact{
		lunarEclipseSVGContactFor("P1", lunarEclipseSVGContactName("P1", language), info.PenumbralStart, angles),
	}
	if info.HasPartial {
		contacts = append(contacts, lunarEclipseSVGContactFor("U1", lunarEclipseSVGContactName("U1", language), info.PartialStart, angles))
	}
	if info.HasTotal {
		contacts = append(contacts, lunarEclipseSVGContactFor("U2", lunarEclipseSVGContactName("U2", language), info.TotalStart, angles))
	}
	contacts = append(contacts, lunarEclipseSVGContact{label: "GE", name: lunarEclipseSVGContactName("Greatest", language), time: info.Maximum})
	if info.HasTotal {
		contacts = append(contacts, lunarEclipseSVGContactFor("U3", lunarEclipseSVGContactName("U3", language), info.TotalEnd, angles))
	}
	if info.HasPartial {
		contacts = append(contacts, lunarEclipseSVGContactFor("U4", lunarEclipseSVGContactName("U4", language), info.PartialEnd, angles))
	}
	contacts = append(contacts, lunarEclipseSVGContactFor("P4", lunarEclipseSVGContactName("P4", language), info.PenumbralEnd, angles))
	return contacts
}

func lunarEclipseSVGContactFor(
	label, name string,
	time time.Time,
	angles map[string]float64,
) lunarEclipseSVGContact {
	angle, ok := angles[label]
	return lunarEclipseSVGContact{
		label:    label,
		name:     name,
		time:     time,
		angle:    angle,
		hasAngle: ok,
	}
}

func lunarEclipseContactAngleMap(points []LunarEclipseContactPoint) map[string]float64 {
	angles := make(map[string]float64, len(points))
	for _, point := range points {
		angles[point.Label] = point.ContactPositionAngle
	}
	return angles
}

func lunarEclipseSVGContactName(label, language string) string {
	if language == lunarEclipseSVGLanguageEnglish {
		switch label {
		case "P1":
			return "Penumbral begins"
		case "U1":
			return "Partial begins"
		case "U2":
			return "Total begins"
		case "Greatest":
			return "Greatest"
		case "U3":
			return "Total ends"
		case "U4":
			return "Partial ends"
		case "P4":
			return "Penumbral ends"
		default:
			return label
		}
	}
	switch label {
	case "P1":
		return "半影始"
	case "U1":
		return "初亏"
	case "U2":
		return "食既"
	case "Greatest":
		return "食甚"
	case "U3":
		return "生光"
	case "U4":
		return "复圆"
	case "P4":
		return "半影终"
	default:
		return label
	}
}

func durationToDays(duration time.Duration) float64 {
	if duration <= 0 {
		return 0
	}
	return duration.Hours() / 24
}
