package svg

import (
	"errors"
	"fmt"
	"math"
	"strings"
	"time"
	"unicode"
	"unicode/utf8"

	"b612.me/astro/internal/svgmap"
	"b612.me/astro/moon"
)

// ErrInvalidStarOccultationPath 表示传入 SVG 渲染器的掩带数据无效。
// ErrInvalidStarOccultationPath reports malformed path data passed to the SVG renderer.
var ErrInvalidStarOccultationPath = errors.New("invalid stellar occultation path")

type starOccultationSVGLayout struct {
	width      float64
	height     float64
	margin     float64
	mapX       float64
	mapY       float64
	mapWidth   float64
	mapHeight  float64
	panelX     float64
	panelY     float64
	panelWidth float64
	footerY    float64
	projection svgmap.Projection
}

type starOccultationGeoPoint struct {
	longitude float64
	latitude  float64
}

type starOccultationEventRow struct {
	name  string
	point moon.OccultationPathPoint
}

func validateStarOccultationPath(path moon.StarOccultationPath) error {
	if !path.Complete {
		return fmt.Errorf("%w: global path is incomplete", ErrInvalidStarOccultationPath)
	}
	if err := validateStarOccultationPathPoint("start", path.Start); err != nil {
		return err
	}
	if err := validateStarOccultationPathPoint("greatest", path.Greatest); err != nil {
		return err
	}
	if err := validateStarOccultationPathPoint("end", path.End); err != nil {
		return err
	}
	if path.Greatest.Time.Before(path.Start.Time) || path.End.Time.Before(path.Greatest.Time) {
		return fmt.Errorf("%w: event times must be ordered start, greatest, end", ErrInvalidStarOccultationPath)
	}
	if err := (moon.OccultationPathOptions{Step: path.Step, TargetSpacingKM: path.TargetSpacingKM}).Validate(); err != nil {
		return fmt.Errorf("%w: invalid path sampling metadata: %v", ErrInvalidStarOccultationPath, err)
	}
	series := []struct {
		name   string
		points []moon.OccultationPathPoint
	}{
		{"center line", path.CenterLine},
		{"northern limit", path.NorthernLimit},
		{"southern limit", path.SouthernLimit},
	}
	for _, current := range series {
		name, points := current.name, current.points
		for index, point := range points {
			if err := validateStarOccultationPathPoint(fmt.Sprintf("%s[%d]", name, index), point); err != nil {
				return err
			}
			if index > 0 && !point.Time.After(points[index-1].Time) {
				return fmt.Errorf("%w: %s times must be strictly increasing", ErrInvalidStarOccultationPath, name)
			}
		}
	}
	if len(path.NorthernLimit) != len(path.SouthernLimit) {
		return fmt.Errorf("%w: northern and southern limits must have the same sample count", ErrInvalidStarOccultationPath)
	}
	if len(path.NorthernLimit) < 2 {
		return fmt.Errorf("%w: global limits must contain start and end", ErrInvalidStarOccultationPath)
	}
	for index := range path.NorthernLimit {
		if !path.NorthernLimit[index].Time.Equal(path.SouthernLimit[index].Time) {
			return fmt.Errorf("%w: northern and southern limit sample %d times must match", ErrInvalidStarOccultationPath, index)
		}
	}
	last := len(path.NorthernLimit) - 1
	if !path.NorthernLimit[0].Time.Equal(path.Start.Time) || !path.SouthernLimit[0].Time.Equal(path.Start.Time) ||
		!path.NorthernLimit[last].Time.Equal(path.End.Time) || !path.SouthernLimit[last].Time.Equal(path.End.Time) {
		return fmt.Errorf("%w: global limits must span start through end", ErrInvalidStarOccultationPath)
	}
	return nil
}

func validateStarOccultationPathPoint(name string, point moon.OccultationPathPoint) error {
	if point.Time.IsZero() {
		return fmt.Errorf("%w: %s time is required", ErrInvalidStarOccultationPath, name)
	}
	if !starOccultationFinite(point.Longitude) || point.Longitude < -180 || point.Longitude > 180 {
		return fmt.Errorf("%w: %s longitude must be in [-180, 180]", ErrInvalidStarOccultationPath, name)
	}
	if !starOccultationFinite(point.Latitude) || point.Latitude < -90 || point.Latitude > 90 {
		return fmt.Errorf("%w: %s latitude must be in [-90, 90]", ErrInvalidStarOccultationPath, name)
	}
	if !starOccultationFinite(point.MoonAltitude) || point.MoonAltitude < -90 || point.MoonAltitude > 90 {
		return fmt.Errorf("%w: %s Moon altitude must be in [-90, 90]", ErrInvalidStarOccultationPath, name)
	}
	if !starOccultationFinite(point.WidthKM) || point.WidthKM < 0 {
		return fmt.Errorf("%w: %s width must be finite and non-negative", ErrInvalidStarOccultationPath, name)
	}
	return nil
}

func starOccultationSVGLayoutFor(
	options StarOccultationSVGOptions,
	headerBottom float64,
	projection svgmap.Projection,
) starOccultationSVGLayout {
	width := float64(options.Width)
	height := float64(options.Height)
	margin := math.Max(22, math.Min(42, width*0.04))
	gap := math.Max(16, math.Min(24, width*0.025))
	panelWidth := math.Max(148, math.Min(238, width*0.23))
	mapWidth := width - 2*margin - gap - panelWidth
	if mapWidth < 220 {
		panelWidth = math.Max(126, width*0.21)
		mapWidth = width - 2*margin - gap - panelWidth
	}
	contentTop := headerBottom + 28
	footerSpace := 82.0
	if projection != svgmap.ProjectionEquirectangular {
		footerSpace = 116
	}
	availableHeight := math.Max(110, height-contentTop-footerSpace)
	mapHeight := math.Min(mapWidth/2, availableHeight)
	if projection != svgmap.ProjectionEquirectangular {
		mapHeight = math.Min(mapWidth, availableHeight)
		mapWidth = mapHeight
	}
	if mapHeight < 110 {
		mapHeight = 110
		mapWidth = math.Min(mapWidth, 2*mapHeight)
	}
	mapY := contentTop + math.Max(0, (availableHeight-mapHeight)/2)
	return starOccultationSVGLayout{
		width:      width,
		height:     height,
		margin:     margin,
		mapX:       margin,
		mapY:       mapY,
		mapWidth:   mapWidth,
		mapHeight:  mapHeight,
		panelX:     margin + mapWidth + gap,
		panelY:     mapY,
		panelWidth: panelWidth,
		footerY:    height - 54,
		projection: projection,
	}
}

func (layout starOccultationSVGLayout) project(longitude, latitude float64) (float64, float64) {
	x, y, _ := layout.mapFrame().Project(longitude, latitude)
	return x, y
}

func (layout starOccultationSVGLayout) mapFrame() svgmap.Frame {
	return svgmap.Frame{
		X:          layout.mapX,
		Y:          layout.mapY,
		Width:      layout.mapWidth,
		Height:     layout.mapHeight,
		Projection: layout.projection,
	}
}

func resolveStarOccultationMapProjection(path moon.StarOccultationPath, requested MapProjection) svgmap.Projection {
	minimumLatitude := path.Greatest.Latitude
	maximumLatitude := path.Greatest.Latitude
	for _, series := range [][]moon.OccultationPathPoint{path.CenterLine, path.NorthernLimit, path.SouthernLimit} {
		for _, point := range series {
			minimumLatitude = math.Min(minimumLatitude, point.Latitude)
			maximumLatitude = math.Max(maximumLatitude, point.Latitude)
		}
	}
	return svgmap.ResolveProjection(internalMapProjection(requested), path.Greatest.Latitude, minimumLatitude, maximumLatitude)
}

func starOccultationPathSegments(points []moon.OccultationPathPoint) [][]moon.OccultationPathPoint {
	if len(points) == 0 {
		return nil
	}
	segments := make([][]moon.OccultationPathPoint, 0, 2)
	current := []moon.OccultationPathPoint{points[0]}
	for index := 1; index < len(points); index++ {
		if math.Abs(points[index].Longitude-points[index-1].Longitude) <= 180 {
			current = append(current, points[index])
			continue
		}
		boundary, fraction, ok := starOccultationAntimeridianCrossing(points[index-1], points[index])
		if !ok {
			current = append(current, points[index])
			continue
		}
		crossing := starOccultationInterpolatePathPoint(points[index-1], points[index], fraction, boundary)
		current = append(current, crossing)
		if len(current) >= 2 {
			segments = append(segments, current)
		}
		wrapped := crossing
		wrapped.Longitude = -boundary
		current = []moon.OccultationPathPoint{wrapped, points[index]}
	}
	if len(current) >= 2 {
		segments = append(segments, current)
	}
	return segments
}

func starOccultationPathSegmentsForProjection(
	points []moon.OccultationPathPoint,
	projection svgmap.Projection,
) [][]moon.OccultationPathPoint {
	if projection == svgmap.ProjectionEquirectangular {
		return starOccultationPathSegments(points)
	}
	geographic := make([]svgmap.GeoPoint, len(points))
	for index, point := range points {
		geographic[index] = svgmap.GeoPoint{Longitude: point.Longitude, Latitude: point.Latitude}
	}
	clipped := svgmap.PolylineSegments(geographic, projection)
	result := make([][]moon.OccultationPathPoint, 0, len(clipped))
	for _, segment := range clipped {
		converted := make([]moon.OccultationPathPoint, len(segment))
		for index, point := range segment {
			converted[index] = moon.OccultationPathPoint{Longitude: point.Longitude, Latitude: point.Latitude}
		}
		result = append(result, converted)
	}
	return result
}

func starOccultationAntimeridianCrossing(a, b moon.OccultationPathPoint) (float64, float64, bool) {
	if math.Abs(b.Longitude-a.Longitude) <= 180 {
		return 0, 0, false
	}
	boundary := 180.0
	adjustedB := b.Longitude
	if a.Longitude < 0 {
		boundary = -180
		adjustedB -= 360
	} else {
		adjustedB += 360
	}
	denominator := adjustedB - a.Longitude
	if math.Abs(denominator) < 1e-12 {
		return 0, 0, false
	}
	fraction := (boundary - a.Longitude) / denominator
	if fraction <= 0 || fraction >= 1 {
		return 0, 0, false
	}
	return boundary, fraction, true
}

func starOccultationInterpolatePathPoint(a, b moon.OccultationPathPoint, fraction, longitude float64) moon.OccultationPathPoint {
	if fraction < 0 {
		fraction = 0
	}
	if fraction > 1 {
		fraction = 1
	}
	duration := b.Time.Sub(a.Time)
	return moon.OccultationPathPoint{
		Time:         a.Time.Add(time.Duration(float64(duration) * fraction)),
		Longitude:    longitude,
		Latitude:     a.Latitude + (b.Latitude-a.Latitude)*fraction,
		MoonAltitude: a.MoonAltitude + (b.MoonAltitude-a.MoonAltitude)*fraction,
		WidthKM:      a.WidthKM + (b.WidthKM-a.WidthKM)*fraction,
	}
}

func starOccultationBandSegments(
	northern, southern []moon.OccultationPathPoint,
) [][]starOccultationGeoPoint {
	return starOccultationBandFragments(northern, southern, svgmap.ProjectionEquirectangular)
}

func starOccultationBandFragments(
	northern, southern []moon.OccultationPathPoint,
	projection svgmap.Projection,
) [][]starOccultationGeoPoint {
	count := len(northern)
	if len(southern) < count {
		count = len(southern)
	}
	if count < 2 {
		return nil
	}
	polygon := make([]starOccultationGeoPoint, 0, 2*count)
	for _, point := range northern[:count] {
		polygon = append(polygon, starOccultationGeoPoint{point.Longitude, point.Latitude})
	}
	for index := count - 1; index >= 0; index-- {
		point := southern[index]
		polygon = append(polygon, starOccultationGeoPoint{point.Longitude, point.Latitude})
	}
	geographic := make([]svgmap.GeoPoint, len(polygon))
	for index, point := range polygon {
		geographic[index] = svgmap.GeoPoint{Longitude: point.longitude, Latitude: point.latitude}
	}
	fragments := svgmap.PolygonFragments(geographic, projection)
	segments := make([][]starOccultationGeoPoint, 0, len(fragments))
	for _, fragment := range fragments {
		converted := make([]starOccultationGeoPoint, len(fragment))
		for index, point := range fragment {
			converted[index] = starOccultationGeoPoint{longitude: point.Longitude, latitude: point.Latitude}
		}
		if math.Abs(starOccultationPolygonArea(converted)) > 1e-9 {
			segments = append(segments, converted)
		}
	}
	return segments
}

func legacyStarOccultationBandSegments(polygon []starOccultationGeoPoint) [][]starOccultationGeoPoint {
	polygon = starOccultationUnwrapPolygon(polygon)
	minimumLongitude, maximumLongitude := polygon[0].longitude, polygon[0].longitude
	for _, point := range polygon[1:] {
		minimumLongitude = math.Min(minimumLongitude, point.longitude)
		maximumLongitude = math.Max(maximumLongitude, point.longitude)
	}
	firstWorld := int(math.Floor((minimumLongitude + 180) / 360))
	lastWorld := int(math.Floor((maximumLongitude + 180) / 360))
	segments := make([][]starOccultationGeoPoint, 0, lastWorld-firstWorld+1)
	for world := firstWorld; world <= lastWorld; world++ {
		left := -180.0 + 360*float64(world)
		right := 180.0 + 360*float64(world)
		clipped := starOccultationClipPolygonLongitude(polygon, left, true)
		clipped = starOccultationClipPolygonLongitude(clipped, right, false)
		if len(clipped) < 3 {
			continue
		}
		for index := range clipped {
			clipped[index].longitude -= 360 * float64(world)
		}
		if math.Abs(starOccultationPolygonArea(clipped)) > 1e-9 {
			segments = append(segments, clipped)
		}
	}
	return segments
}

func starOccultationUnwrapPolygon(points []starOccultationGeoPoint) []starOccultationGeoPoint {
	if len(points) < 2 {
		return points
	}
	unwrapped := make([]starOccultationGeoPoint, len(points))
	unwrapped[0] = points[0]
	for index := 1; index < len(points); index++ {
		point := points[index]
		previous := unwrapped[index-1].longitude
		for point.longitude-previous > 180 {
			point.longitude -= 360
		}
		for point.longitude-previous < -180 {
			point.longitude += 360
		}
		unwrapped[index] = point
	}
	return unwrapped
}

func starOccultationClipPolygonLongitude(
	points []starOccultationGeoPoint,
	boundary float64,
	keepGreater bool,
) []starOccultationGeoPoint {
	if len(points) == 0 {
		return nil
	}
	inside := func(point starOccultationGeoPoint) bool {
		if keepGreater {
			return point.longitude >= boundary
		}
		return point.longitude <= boundary
	}
	intersect := func(a, b starOccultationGeoPoint) starOccultationGeoPoint {
		fraction := (boundary - a.longitude) / (b.longitude - a.longitude)
		return starOccultationGeoPoint{
			longitude: boundary,
			latitude:  a.latitude + (b.latitude-a.latitude)*fraction,
		}
	}
	clipped := make([]starOccultationGeoPoint, 0, len(points)+2)
	previous := points[len(points)-1]
	previousInside := inside(previous)
	for _, current := range points {
		currentInside := inside(current)
		if currentInside != previousInside {
			clipped = append(clipped, intersect(previous, current))
		}
		if currentInside {
			clipped = append(clipped, current)
		}
		previous = current
		previousInside = currentInside
	}
	return clipped
}

func starOccultationPolygonArea(points []starOccultationGeoPoint) float64 {
	area := 0.0
	for index, point := range points {
		next := points[(index+1)%len(points)]
		area += point.longitude*next.latitude - next.longitude*point.latitude
	}
	return area / 2
}

func starOccultationSVGHeaderLines(path moon.StarOccultationPath, options StarOccultationSVGOptions) []string {
	lines := make([]string, 0, 4)
	for _, item := range []struct {
		text     string
		fontSize float64
	}{
		{starOccultationSVGSummaryText(path, options), 14},
		{starOccultationSVGGreatestText(path, options), 13},
	} {
		lines = append(lines, starOccultationWrapText(item.text, float64(options.Width)-80, item.fontSize)...)
	}
	return lines
}

func starOccultationSVGTitle(path moon.StarOccultationPath, options StarOccultationSVGOptions) string {
	if options.Title != "" {
		return options.Title
	}
	target := path.TargetID
	if target == "" {
		if options.Language == starOccultationSVGLanguageEnglish {
			target = "star"
		} else {
			target = "恒星"
		}
	}
	date := path.Greatest.Time.In(options.Location).Format("2006-01-02")
	if options.Language == starOccultationSVGLanguageEnglish {
		return fmt.Sprintf("%s Lunar Occultation of %s", date, target)
	}
	return fmt.Sprintf("%s 月掩%s全球掩带", date, target)
}

func starOccultationSVGSummaryText(path moon.StarOccultationPath, options StarOccultationSVGOptions) string {
	if options.SummaryText != "" {
		return options.SummaryText
	}
	start := path.Start.Time.In(options.Location)
	greatest := path.Greatest.Time.In(options.Location)
	end := path.End.Time.In(options.Location)
	zone := starOccultationLocationLabel(greatest, options.Location)
	if options.Language == starOccultationSVGLanguageEnglish {
		return fmt.Sprintf("Start %s | Greatest %s | End %s (%s)",
			starOccultationFormatEventTime(start, true),
			starOccultationFormatEventTime(greatest, !starOccultationSameDate(start, greatest)),
			starOccultationFormatEventTime(end, !starOccultationSameDate(start, end)), zone)
	}
	return fmt.Sprintf("掩始 %s | 掩甚 %s | 掩终 %s (%s)",
		starOccultationFormatEventTime(start, true),
		starOccultationFormatEventTime(greatest, !starOccultationSameDate(start, greatest)),
		starOccultationFormatEventTime(end, !starOccultationSameDate(start, end)), zone)
}

func starOccultationSVGGreatestText(path moon.StarOccultationPath, options StarOccultationSVGOptions) string {
	if options.GreatestText != "" {
		return options.GreatestText
	}
	coordinates := starOccultationFormatCoordinates(path.Greatest.Longitude, path.Greatest.Latitude)
	altitude := starOccultationFormatSignedDegree(path.Greatest.MoonAltitude)
	if options.Language == starOccultationSVGLanguageEnglish {
		return fmt.Sprintf("Greatest point %s | path width %.1f km | Moon altitude %s", coordinates, path.Greatest.WidthKM, altitude)
	}
	return fmt.Sprintf("掩甚点 %s | 掩带宽 %.1f km | 月球高度 %s", coordinates, path.Greatest.WidthKM, altitude)
}

func starOccultationSVGMapTitle(options StarOccultationSVGOptions) string {
	if options.MapTitle != "" {
		return options.MapTitle
	}
	if options.Language == starOccultationSVGLanguageEnglish {
		return "Global center line and occultation limits"
	}
	return "全球中心线与掩带边界"
}

func starOccultationSVGContactsTitle(options StarOccultationSVGOptions) string {
	if options.ContactsTitle != "" {
		return options.ContactsTitle
	}
	if options.Language == starOccultationSVGLanguageEnglish {
		return "Global events"
	}
	return "全球事件"
}

func starOccultationSVGFooter(options StarOccultationSVGOptions) string {
	if options.FooterNote != "" {
		return options.FooterNote
	}
	projection := starOccultationProjectionLabel(options.Projection, options.Language)
	if options.Language == starOccultationSVGLanguageEnglish {
		return fmt.Sprintf("%s with Natural Earth 1:50m physical land and no administrative boundaries. Limits use the outer lunar limb on the Earth ellipsoid.", projection)
	}
	return fmt.Sprintf("%s；Natural Earth 1:50m 物理陆地底图，不含行政边界；掩带边界为地球椭球上的月球外缘投影。", projection)
}

func starOccultationProjectionLabel(projection MapProjection, language string) string {
	if language == starOccultationSVGLanguageEnglish {
		switch projection {
		case MapProjectionNorthPolar:
			return "North-polar azimuthal equidistant projection"
		case MapProjectionSouthPolar:
			return "South-polar azimuthal equidistant projection"
		default:
			return "Equirectangular projection"
		}
	}
	switch projection {
	case MapProjectionNorthPolar:
		return "北极方位等距投影"
	case MapProjectionSouthPolar:
		return "南极方位等距投影"
	default:
		return "等经纬投影"
	}
}

func starOccultationSVGEventRows(path moon.StarOccultationPath, language string) []starOccultationEventRow {
	names := []string{"掩始", "掩甚", "掩终"}
	if language == starOccultationSVGLanguageEnglish {
		names = []string{"Start", "Greatest", "End"}
	}
	return []starOccultationEventRow{
		{name: names[0], point: path.Start},
		{name: names[1], point: path.Greatest},
		{name: names[2], point: path.End},
	}
}

func starOccultationFormatEventTime(value time.Time, withDate bool) string {
	layout := "15:04:05.0"
	if withDate {
		layout = "2006-01-02 15:04:05.0"
	}
	return value.Format(layout)
}

func starOccultationFormatCoordinates(longitude, latitude float64) string {
	longitudeSuffix := "E"
	if longitude < 0 {
		longitudeSuffix = "W"
	}
	latitudeSuffix := "N"
	if latitude < 0 {
		latitudeSuffix = "S"
	}
	return fmt.Sprintf("%.4f°%s, %.4f°%s", math.Abs(longitude), longitudeSuffix, math.Abs(latitude), latitudeSuffix)
}

func starOccultationFormatSignedDegree(value float64) string {
	return fmt.Sprintf("%+.1f°", value)
}

func starOccultationLocationLabel(value time.Time, location *time.Location) string {
	if location == time.UTC {
		return "UTC"
	}
	name, offset := value.Zone()
	if name != "" && name != "Local" {
		return name
	}
	hours := float64(offset) / 3600
	return fmt.Sprintf("UTC%+.1f", hours)
}

func starOccultationSameDate(first, second time.Time) bool {
	y1, m1, d1 := first.Date()
	y2, m2, d2 := second.Date()
	return y1 == y2 && m1 == m2 && d1 == d2
}

func starOccultationWrapText(value string, maxWidth, fontSize float64) []string {
	value = strings.TrimSpace(value)
	if value == "" {
		return nil
	}
	if starOccultationTextWidth(value, fontSize) <= maxWidth {
		return []string{value}
	}
	runes := []rune(value)
	lines := make([]string, 0, 2)
	for len(runes) > 0 {
		width := 0.0
		end := 0
		lastSpace := -1
		for end < len(runes) {
			nextWidth := width + starOccultationRuneWidth(runes[end], fontSize)
			if nextWidth > maxWidth && end > 0 {
				break
			}
			width = nextWidth
			if unicode.IsSpace(runes[end]) {
				lastSpace = end
			}
			end++
		}
		if end < len(runes) && lastSpace > 0 {
			end = lastSpace
		}
		if end == 0 {
			end = 1
		}
		line := strings.TrimSpace(string(runes[:end]))
		if line != "" {
			lines = append(lines, line)
		}
		runes = runes[end:]
		for len(runes) > 0 && unicode.IsSpace(runes[0]) {
			runes = runes[1:]
		}
	}
	return lines
}

func starOccultationTitleFontSize(title string, width float64) int {
	for size := 26; size >= 16; size-- {
		if starOccultationTextWidth(title, float64(size)) <= width-80 {
			return size
		}
	}
	return 16
}

func starOccultationTextWidth(value string, fontSize float64) float64 {
	width := 0.0
	for _, current := range value {
		width += starOccultationRuneWidth(current, fontSize)
	}
	return width
}

func starOccultationRuneWidth(value rune, fontSize float64) float64 {
	if unicode.Is(unicode.Han, value) || value > utf8.RuneSelf {
		return fontSize
	}
	if unicode.IsSpace(value) {
		return fontSize * 0.34
	}
	return fontSize * 0.58
}

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