package locationoccurrences import ( "fmt" "reflect" "sort" "gitea.maximumdirect.net/eric/notarius/internal/core/source" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd" locationregistry "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/locations/registry" "gitea.maximumdirect.net/eric/notarius/internal/modules/dnd/shared" ) type orderedOccurrence struct { value dnd.LocationOccurrence earliest int hasEvidence bool } func canonicalOccurrenceList(response extractionResponse, order shared.SourceRefOrder, sourceID string, grounding *locationregistry.Grounding) (dnd.LocationOccurrenceList, error) { if response.Occurrences == nil { return dnd.LocationOccurrenceList{}, nil } ordered := make([]orderedOccurrence, len(response.Occurrences)) for index, occurrence := range response.Occurrences { location, ok := grounding.Resolve(locationregistry.Selector{Name: occurrence.Name, RegistryRefs: occurrence.RegistryRefs}) if !ok { return dnd.LocationOccurrenceList{}, fmt.Errorf("occurrence %d does not match a supplied location selector", index) } refs := order.Canonicalize(canonicalSourceRefs(occurrence.SourceRefs, sourceID)) earliest, hasEvidence := order.EarliestValid(refs) ordered[index] = orderedOccurrence{value: dnd.LocationOccurrence{ LocationID: location.ID, Name: location.Name, Kind: dnd.LocationOccurrenceKind(occurrence.Kind), SourceRefs: refs, }, earliest: earliest, hasEvidence: hasEvidence} } sort.SliceStable(ordered, func(left, right int) bool { return lessOccurrence(ordered[left], ordered[right], order) }) occurrences := make([]dnd.LocationOccurrence, 0, len(ordered)) for _, occurrence := range ordered { if len(occurrences) == 0 || !sameOccurrence(occurrences[len(occurrences)-1], occurrence.value) { occurrences = append(occurrences, occurrence.value) } } return dnd.LocationOccurrenceList{Occurrences: occurrences}, nil } func lessOccurrence(left, right orderedOccurrence, order shared.SourceRefOrder) bool { if left.hasEvidence != right.hasEvidence { return left.hasEvidence } if left.hasEvidence && left.earliest != right.earliest { return left.earliest < right.earliest } if left.value.LocationID != right.value.LocationID { return left.value.LocationID < right.value.LocationID } if left.value.Name != right.value.Name { return left.value.Name < right.value.Name } if kindOrder(left.value.Kind) != kindOrder(right.value.Kind) { return kindOrder(left.value.Kind) < kindOrder(right.value.Kind) } return lessReferences(left.value.SourceRefs, right.value.SourceRefs, order) } func kindOrder(kind dnd.LocationOccurrenceKind) int { switch kind { case dnd.LocationOccurrenceKindVisited: return 0 case dnd.LocationOccurrenceKindPlanned: return 1 case dnd.LocationOccurrenceKindRecalled: return 2 case dnd.LocationOccurrenceKindMentioned: return 3 default: return 4 } } func lessReferences(left, right []source.SourceRef, order shared.SourceRefOrder) bool { limit := len(left) if len(right) < limit { limit = len(right) } for index := 0; index < limit; index++ { if left[index] == right[index] { continue } return order.Less(left[index], right[index]) } return len(left) < len(right) } func sameOccurrence(left, right dnd.LocationOccurrence) bool { return left.LocationID == right.LocationID && left.Name == right.Name && left.Kind == right.Kind && reflect.DeepEqual(left.SourceRefs, right.SourceRefs) } func canonicalSourceRefs(values []occurrenceSourceRefResponse, sourceID string) []source.SourceRef { if values == nil { return nil } refs := make([]source.SourceRef, len(values)) for index, value := range values { refs[index] = source.SourceRef{SourceID: sourceID, StartUnitID: value.StartUnitID, EndUnitID: value.EndUnitID} } return refs }