🏡 index : quine-global/quintodrome.git

package model

import (
	"slices"

	"github.com/navidrome/navidrome/utils/gg"
)

func normalizeLyrics(lyrics Lyrics) Lyrics {
	lyrics.Line = normalizeCueLines(lyrics.Line)
	if len(lyrics.Agents) == 0 {
		lyrics.Agents = nil
	}
	return lyrics
}

func normalizeCueLines(lines []Line) []Line {
	if len(lines) == 0 {
		return lines
	}

	normalized := make([]Line, len(lines))
	copy(normalized, lines)

	for i := range normalized {
		if len(normalized[i].Cue) > 0 {
			normalized[i].Cue = slices.Clone(normalized[i].Cue)
		}

		var fallbackEnd *int64
		if normalized[i].End != nil {
			v := *normalized[i].End
			fallbackEnd = &v
		} else if i+1 < len(normalized) && normalized[i+1].Start != nil {
			v := *normalized[i+1].Start
			fallbackEnd = &v
		}

		normalized[i] = normalizeCueLine(normalized[i], fallbackEnd)
	}

	return normalized
}

func normalizeLineTiming(line Line) Line {
	if len(line.Cue) == 0 {
		return line
	}

	var earliestStart *int64
	var latestEnd *int64
	for i := range line.Cue {
		token := line.Cue[i]
		if token.Start != nil {
			if earliestStart == nil || *token.Start < *earliestStart {
				v := *token.Start
				earliestStart = &v
			}
		}

		candidateEnd := token.End
		if candidateEnd == nil {
			candidateEnd = token.Start
		}
		if candidateEnd != nil {
			if latestEnd == nil || *candidateEnd > *latestEnd {
				v := *candidateEnd
				latestEnd = &v
			}
		}
	}

	if line.Start == nil && earliestStart != nil {
		v := *earliestStart
		line.Start = &v
	}
	if line.End == nil && latestEnd != nil {
		v := *latestEnd
		line.End = &v
	}
	return line
}

func normalizeCueLine(line Line, fallbackEnd *int64) Line {
	if len(line.Cue) == 0 {
		return line
	}
	line.Cue = normalizeCueEndsByAgent(line.Cue, fallbackEnd)
	return normalizeLineTiming(line)
}

// normalizeCueEndsByAgent resolves cue end times independently per agent so that
// background (or other parallel) layers, whose cues interleave with the main
// timeline but are stored together in document order, do not clamp each other's
// ends. Each agent group is normalized in its own document order; results are
// reassembled into the original cue positions.
func normalizeCueEndsByAgent(cues []Cue, fallbackEnd *int64) []Cue {
	groups := make(map[string][]int)
	order := make([]string, 0, 2)
	for i := range cues {
		id := cues[i].AgentID
		if _, ok := groups[id]; !ok {
			order = append(order, id)
		}
		groups[id] = append(groups[id], i)
	}

	// Single agent: the document order already matches the timeline, so the
	// straightforward normalization applies without regrouping.
	if len(order) <= 1 {
		return NormalizeCueEnds(cues, fallbackEnd)
	}

	out := slices.Clone(cues)
	for _, id := range order {
		idxs := groups[id]
		group := make([]Cue, len(idxs))
		for gi, pos := range idxs {
			group[gi] = cues[pos]
		}
		group = NormalizeCueEnds(group, fallbackEnd)
		for gi, pos := range idxs {
			out[pos] = group[gi]
		}
	}
	return out
}

// NormalizeCueEnds resolves missing cue end times within a single ordered cue
// group: each end is filled from the next cue's start, then from fallbackEnd,
// and is clamped so it never precedes the cue's own start nor overruns the next
// cue. End times are all-or-none — if any cue still lacks an end afterwards, all
// ends in the group are cleared. The input slice is never mutated.
//
// Exported because the Subsonic enhanced-lyrics serializer resolves cue ends
// per agent group while building the response; all other normalization is
// package-internal.
func NormalizeCueEnds(cues []Cue, fallbackEnd *int64) []Cue {
	if len(cues) == 0 {
		return cues
	}

	out := slices.Clone(cues)
	for i := range out {
		end := out[i].End
		if end == nil {
			if i+1 < len(out) && out[i+1].Start != nil {
				end = out[i+1].Start
			} else {
				end = fallbackEnd
			}
		}
		if end != nil && i+1 < len(out) && out[i+1].Start != nil && *end > *out[i+1].Start {
			end = out[i+1].Start
		}
		if end != nil && out[i].Start != nil && *end < *out[i].Start {
			end = out[i].Start
		}
		out[i].End = gg.Clone(end)
	}

	for i := range out {
		if out[i].End == nil {
			for j := range out {
				out[j].End = nil
			}
			break
		}
	}
	return out
}