// Lazily built inverted search indexes on the Snapshot: full-text (BM25)
// and geo-spatial (k-d tree). Each (label, field) index builds on first
// query and caches; the shared index is cloned out under a brief lock so
// the query itself runs off the mutex, and a racing build is discarded.

package chickpeas

import 

type geoKey struct {
	label  Label
	latKey PropertyKey
	lonKey PropertyKey
}

// FullTextSearch returns the nodes of label whose key string property
// contains every token in query (boolean AND); empty for an unknown
// label/key, an empty query, or a token no document contains. The result
// composes with NodesWithLabel and other sets via And/Or/AndNot.
func ( *Snapshot) (, ,  string) *nodeset.Set {
	,  := .fulltextField(, )
	if ! {
		return nodeset.New()
	}
	return .Query()
}

// FullTextSearchRanked returns the top k nodes of label by BM25 relevance
// of their key property to query (disjunctive), sorted by score
// descending, ties by ascending node id.
func ( *Snapshot) (, ,  string,  int) []RankedHit {
	,  := .fulltextField(, )
	if ! {
		return nil
	}
	return .QueryRanked(, )
}

func ( *Snapshot) (,  string) (*FullTextField, bool) {
	,  := .Label()
	if ! {
		return nil, false
	}
	,  := .PropertyKey()
	if ! {
		return nil, false
	}
	 := propIndexKey{label: , key: }
	.fulltextMu.Lock()
	,  := .fulltextIndex[]
	.fulltextMu.Unlock()
	if  {
		return , true
	}
	 := .buildFullTextField(, )
	.fulltextMu.Lock()
	defer .fulltextMu.Unlock()
	if ,  := .fulltextIndex[];  {
		return , true
	}
	.fulltextIndex[] = 
	return , true
}

// buildFullTextField scans the string column, tokenizing each labelled
// node's value.
func ( *Snapshot) ( Label,  PropertyKey) *FullTextField {
	 := .labelIndex[]
	,  := .columns[]
	if ! ||  == nil {
		return BuildFullTextField(func(func(uint32, string) bool) {})
	}
	return BuildFullTextField(func( func(uint32, string) bool) {
		for ,  := range .Entries() {
			,  := .StrID()
			if ! || !.Contains() {
				continue
			}
			,  := .atoms.Resolve()
			if ! {
				continue
			}
			if !(, ) {
				return
			}
		}
	})
}

// GeoWithinRadius returns the nodes of label within km great-circle
// distance of (lat, lon), reading the latKey/lonKey f64 properties. The
// per-field index builds lazily and caches.
func ( *Snapshot) (, ,  string, , ,  float64) *nodeset.Set {
	,  := .geoIndexFor(, , )
	if ! {
		return nodeset.New()
	}
	return .WithinRadius(, , )
}

// GeoWithinBBox returns the nodes of label whose coordinates fall in the
// lat/lon rectangle; minLon > maxLon crosses the antimeridian.
func ( *Snapshot) (, ,  string, , , ,  float64) *nodeset.Set {
	,  := .geoIndexFor(, , )
	if ! {
		return nodeset.New()
	}
	return .WithinBBox(, , , )
}

// GeoKNN returns the k nodes of label nearest (lat, lon), sorted by
// increasing distance, ties by node id.
func ( *Snapshot) (, ,  string, ,  float64,  int) []GeoHit {
	,  := .geoIndexFor(, , )
	if ! {
		return nil
	}
	return .KNN(, , )
}

func ( *Snapshot) (, ,  string) (*GeoIndex, bool) {
	,  := .Label()
	if ! {
		return nil, false
	}
	,  := .PropertyKey()
	if ! {
		return nil, false
	}
	,  := .PropertyKey()
	if ! {
		return nil, false
	}
	 := geoKey{label: , latKey: , lonKey: }
	.geoMu.Lock()
	,  := .geoIndex[]
	.geoMu.Unlock()
	if  {
		return , true
	}
	 := .buildGeoIndex(, , )
	.geoMu.Lock()
	defer .geoMu.Unlock()
	if ,  := .geoIndex[];  {
		return , true
	}
	.geoIndex[] = 
	return , true
}

// buildGeoIndex reads each labelled node's lat/lon f64 properties.
func ( *Snapshot) ( Label, ,  PropertyKey) *GeoIndex {
	 := .labelIndex[]
	,  := .columns[]
	,  := .columns[]
	if ! || ! ||  == nil {
		return BuildGeoIndex(func(func(uint32, float64, float64) bool) {})
	}
	return BuildGeoIndex(func( func(uint32, float64, float64) bool) {
		for ,  := range .Entries() {
			if !.Contains() {
				continue
			}
			,  := .F64()
			if ! {
				continue
			}
			,  := .Get()
			if ! {
				continue
			}
			,  := .F64()
			if ! {
				continue
			}
			if !(, , ) {
				return
			}
		}
	})
}