// Builder finalization: the mutable staging area becomes an immutable
// Snapshot -- parallel CSR build (both directions), label/type indexes, and
// per-column dense / rank-select / sparse storage selection. The rank/
// select thresholds and the str dense rule mirror the Rust finalize;
// i64/f64/bool dense selection deliberately diverges to full-coverage-only
// (missingness, tasks/041 -- mirror task filed in the Rust repo), so a
// staged input with partially-filled numeric/bool columns no longer
// finalizes byte-identically with Rust until they converge.

package chickpeas

import (
	
	

	
	
	
	
)

// rankSelectMinLen is the minimum column span before a moderately-sparse
// column is stored rank/select instead of binary-searched sparse. Below it
// the sparse array stays cache-friendly and the rank index isn't worth its
// cost.
const rankSelectMinLen = 1_000_000

// rankSelectWorth: the span must be large enough to matter, and the
// rank/select layout (presence bits + packed valueBits-bit values) must not
// use more memory than the sparse pair array (sparsePairBytes per entry,
// including alignment padding).
func rankSelectWorth(, , ,  int) bool {
	if  < rankSelectMinLen {
		return false
	}
	 :=  + *
	 :=  *  * 8
	return  <= 
}

// sortPairsLastWriteWins sorts staged pairs by position keeping the LAST
// staged write per position, so re-set properties resolve deterministically
// to the newest value (the dense fill loop already behaves this way).
func sortPairsLastWriteWins[ any]( [],  func() uint32) [] {
	sort.SliceStable(, func(,  int) bool { return ([]) < ([]) })
	 := [:0]
	for ,  := range  {
		if +1 < len() && ([+1]) == () {
			continue // a later write for the same position follows
		}
		 = append(, )
	}
	return 
}

// denseThreshold (i64/f64/bool): only a FULL column (every position
// staged) stores dense. Those dense layouts have no presence
// representation -- an in-range read always reports present -- so storing
// a partially-filled column dense silently turns "missing" into the zero
// value, and observable property semantics (IS NULL, comparisons,
// aggregates, the monotonic walk) would shift with the storage heuristic.
// A partial column goes to rank-select or sparse, which represent absence
// exactly. This diverges from the Rust finalize's historical >=80% rule
// (tasks/041; a mirror task is filed in the Rust repo). Reading a legacy
// file whose dense column was written at partial fill still reports the
// destroyed positions as present-zero: that information is gone.
//
// pairCount counts staged writes, so duplicate writes to one position can
// reach span without full coverage; the column builders confirm with
// coversAllPositions before committing to dense.
func denseThreshold(,  int) bool {
	return  >= 
}

// denseStrThreshold: str keeps the historical >=80% rule -- the dense str
// layout encodes missing as atom 0 by design (the read layer folds the
// empty-string check into Prop.Str), so partial fill loses nothing that
// the format doesn't already conflate. The gate counts DISTINCT staged
// positions, not writes (the caller passes distinctPositions), matching
// the Rust finalize's tasks/237 rule so str selection stays
// byte-identical across the two writers.
func denseStrThreshold(,  int) bool {
	return  >= int(float64()*0.8)
}

// distinctPositions counts the distinct positions the pairs touch -- the
// dense gates' real quantity: a staged-write count can reach any
// threshold through duplicate writes to a few positions while the fill
// stays tiny (rustychickpeas tasks/237).
func distinctPositions[ interface{ () uint32 }]( [],  int) int {
	 := bitset.New()
	 := 0
	for ,  := range  {
		if  := int(.()); !.Get() {
			.Set(, true)
			++
		}
	}
	return 
}

// coversAllPositions reports whether the pairs touch every position of the
// span (the exactness check behind the dense selection).
func coversAllPositions[ interface{ () uint32 }]( [],  int) bool {
	return distinctPositions(, ) == 
}

func columnFromPairsI64( []i64Pair,  int) Column {
	if denseThreshold(len(), ) && coversAllPositions(, ) {
		 := make([]int64, )
		for ,  := range  {
			[.id] = .val
		}
		return narrowI64Column()
	}
	 = sortPairsLastWriteWins(, func( i64Pair) uint32 { return .id })
	,  := make([]uint32, len()), make([]int64, len())
	for ,  := range  {
		[], [] = .id, .val
	}
	if rankSelectWorth(, len(), 64, 16) {
		if ,  := narrowI64Vals();  {
			return rankI64NarrowCol{rankIndex: buildRankIndex(, ), narrowI64Vec: }
		}
		return rankI64Col{rankIndex: buildRankIndex(, ), vals: }
	}
	if ,  := narrowI64Vals();  {
		return sparseI64NarrowCol{ids: , narrowI64Vec: }
	}
	return sparseI64Col{ids: , vals: }
}

func columnFromPairsF64( []f64Pair,  int) Column {
	if denseThreshold(len(), ) && coversAllPositions(, ) {
		 := make(denseF64Col, )
		for ,  := range  {
			[.id] = .val
		}
		return 
	}
	 = sortPairsLastWriteWins(, func( f64Pair) uint32 { return .id })
	,  := make([]uint32, len()), make([]float64, len())
	for ,  := range  {
		[], [] = .id, .val
	}
	if rankSelectWorth(, len(), 64, 16) {
		return rankF64Col{rankIndex: buildRankIndex(, ), vals: }
	}
	return sparseF64Col{ids: , vals: }
}

func columnFromPairsBool( []boolPair,  int) Column {
	if denseThreshold(len(), ) && coversAllPositions(, ) {
		 := bitset.New()
		for ,  := range  {
			.Set(int(.id), .val)
		}
		return denseBoolCol{bits: }
	}
	 = sortPairsLastWriteWins(, func( boolPair) uint32 { return .id })
	 := make([]uint32, len())
	for ,  := range  {
		[] = .id
	}
	if rankSelectWorth(, len(), 1, 8) {
		 := bitset.New(len())
		for ,  := range  {
			.Set(, .val)
		}
		return rankBoolCol{rankIndex: buildRankIndex(, ), vals: }
	}
	 := make([]bool, len())
	for ,  := range  {
		[] = .val
	}
	return sparseBoolCol{ids: , vals: }
}

func columnFromPairsStr( []strPair,  int) Column {
	if denseStrThreshold(distinctPositions(, ), ) {
		 := make(denseStrCol, )
		for ,  := range  {
			[.id] = .val
		}
		return 
	}
	 = sortPairsLastWriteWins(, func( strPair) uint32 { return .id })
	,  := make([]uint32, len()), make([]uint32, len())
	for ,  := range  {
		[], [] = .id, .val
	}
	if rankSelectWorth(, len(), 32, 8) {
		return rankStrCol{rankIndex: buildRankIndex(, ), vals: }
	}
	return sparseStrCol{ids: , vals: }
}

// buildCSRDirection fills one CSR direction by counting sort, preserving
// rel insertion order within each node's range, plus the rel-index -> CSR
// position map.
func buildCSRDirection( int,  [][2]NodeID,  []RelType,  []uint32,  int) (
	 []uint32,  []NodeID,  []RelType,  []uint32) {
	 := len()
	 = make([]uint32, +1)
	for  := range  {
		[+1] = [] + []
	}
	 = make([]NodeID, )
	 = make([]RelType, )
	 = make([]uint32, )
	 := make([]uint32, )
	copy(, [:])
	for ,  := range  {
		,  := [], [1-]
		 := []
		[] = 
		[] = []
		[] = 
		[]++
	}
	return , , , 
}

// buildTypeIndex indexes rels by outgoing-CSR position per type (FORMAT.md
// section 4), built from the finalized outTypes so it stays correct when rels
// are staged out of source order -- mirroring the Rust fix (rustychickpeas
// 96243bb).
func buildTypeIndex( *Snapshot) {
	 := map[RelType][]uint32{}
	for ,  := range .outTypes {
		[] = append([], uint32())
	}
	for ,  := range  {
		 := roaring.New()
		.AddMany() // ascending CSR order, duplicate-free
		.typeIndex[] = nodeset.FromBitmap()
	}
}

// finalizeLabels builds the per-label node bitmaps, sharing the source
// snapshot's bitmap for every label whose membership is untouched. With no
// dirty label the whole index aliases and the O(n) label scan is skipped.
func ( *Builder) ( *Snapshot,  int,  *aliasPlan) {
	if .src != nil && .labels {
		maps.Copy(.labelIndex, .src.labelIndex)
		.allLabels = true
		return
	}
	 := map[Label][]uint32{}
	for ,  := range .nodeLabels[:min(, len(.nodeLabels))] {
		for ,  := range  {
			if .labelClean() {
				continue
			}
			[] = append([], uint32())
		}
	}
	if .src != nil {
		for ,  := range .src.labelIndex {
			if .labelClean() {
				.labelIndex[] = 
				.aliasedLabels[] = struct{}{}
			}
		}
	}
	for ,  := range  {
		 := roaring.New()
		.AddMany() // ascending and deduped by construction order
		.labelIndex[] = nodeset.FromBitmap()
	}
}

// finalizeNodeColumns stores every staged node column, sharing the source
// snapshot's column for each untouched key.
func finalizeNodeColumns[ any](
	 *Snapshot,  *aliasPlan,  map[PropertyKey][],  int,  func([], int) Column) {
	for ,  := range  {
		if ,  := .nodeColumn();  {
			.columns[] = 
			continue
		}
		.columns[] = (, )
	}
}

// stagedLens is a staged column family reduced to per-key pair counts,
// feeding the cross-typed drop diagnostic.
func stagedLens[ any]( map[PropertyKey][]) map[PropertyKey]int {
	if len() == 0 {
		return nil
	}
	 := make(map[PropertyKey]int, len())
	for ,  := range  {
		[] = len()
	}
	return 
}

// crossTypedDropCount counts the staged pairs a multi-kind key discards:
// the column families finalize in a fixed order into one column map, so
// for a key staged under several kinds only the LAST family's pairs
// survive and every earlier family's pairs are dropped. Families must be
// passed in finalize order.
func crossTypedDropCount( ...map[PropertyKey]int) int {
	 := map[PropertyKey]int{}
	 := map[PropertyKey]int{}
	 := map[PropertyKey]bool{}
	for ,  := range  {
		for ,  := range  {
			if ,  := [];  {
				[] = true
			}
			[] += 
			[] = 
		}
	}
	 := 0
	for  := range  {
		 += [] - []
	}
	return 
}

// finalizeRelColumns stores every staged rel column, sharing the source's for
// each untouched key. A rebuilt column remaps its staged rel indexes to
// outgoing-CSR positions, the addressing the snapshot stores rel properties
// under.
func finalizeRelColumns[ relPair[]](
	 *Snapshot,  *aliasPlan,  map[PropertyKey][],  int,
	 []uint32,  func([], int) Column) {
	for ,  := range  {
		if ,  := .relColumn();  {
			.relColumns[] = 
			continue
		}
		 := make([], len())
		for ,  := range  {
			[] = .withPairID([.pairID()])
		}
		.relColumns[] = (, )
	}
}

// Finalize consumes the builder into an immutable Snapshot; the builder
// must not be used afterwards. indexProperties optionally names property
// keys whose (label, key) equality indexes are built upfront (faster first
// queries, more memory); all others build lazily on first access.
//
// A builder thawed from a snapshot rebuilds only the components it dirtied
// and shares the rest with its source (cow.go), so a property-only edit skips
// the O(m) CSR rebuild and every untouched column, and the source's lazy
// caches carry forward.
func ( *Builder) ( ...string) *Snapshot {
	// Fold pending removals (rel tombstones + detach-delete cascades) into
	// the staging state first; a no-op when nothing was removed.
	.compactRemovals()
	// The CSR id space covers 0..=maxUsedNodeID (at least one slot).
	 := 1
	if !.knownNodes.IsEmpty() {
		 = int(.knownNodes.Maximum()) + 1
	}
	 := len(.rels)

	 := newSnapshot()
	.nNodes = uint32(.knownNodes.GetCardinality())
	.nRels = uint64()
	.version = .version
	// Retain the known-nodes set: on a sparse id space it is the only
	// record distinguishing nodes from gaps (NodeExists).
	.existence = nodeset.FromBitmap(.knownNodes.Clone())

	 := .newAliasPlan(, )
	// Rel properties are stored by outgoing-CSR position, so the successor
	// needs the incoming -> outgoing map whenever it has any.
	 := len(.relColI64)+len(.relColF64)+len(.relColBool)+len(.relColStr) > 0

	var  []uint32
	if .csr {
		.aliasCSR(, )
		 = .srcRelToOutCSR
		.finalizeLabels(, , )
	} else {
		// The three build phases are independent pure reads of the staging
		// state, so they run as a parallel join; results are deterministic.
		parallel.Join(
			func() {
				.outOffsets, .outNbrs, .outTypes,  =
					buildCSRDirection(, .rels, .relTypes, .degOut, 0)
			},
			func() {
				// The in-direction rel->CSR map fed only the eager inToOut
				// build, now lazy (getInToOut re-derives from the CSR).
				.inOffsets, .inNbrs, .inTypes, _ =
					buildCSRDirection(, .rels, .relTypes, .degIn, 1)
			},
			func() {
				.finalizeLabels(, , )
			},
		)
		buildTypeIndex()
		// inToOut is built lazily on the first rel-position read (getInToOut),
		// not materialized here -- a loaded graph never queried for rel
		// properties keeps its ~64MB (SF1). Record only whether it is needed.
		.hasRelProps = 
	}

	finalizeNodeColumns(, , .nodeColI64, , columnFromPairsI64)
	finalizeNodeColumns(, , .nodeColF64, , columnFromPairsF64)
	finalizeNodeColumns(, , .nodeColBool, , columnFromPairsBool)
	finalizeNodeColumns(, , .nodeColStr, , columnFromPairsStr)

	finalizeRelColumns(, , .relColI64, , , columnFromPairsI64)
	finalizeRelColumns(, , .relColF64, , , columnFromPairsF64)
	finalizeRelColumns(, , .relColBool, , , columnFromPairsBool)
	finalizeRelColumns(, , .relColStr, , , columnFromPairsStr)

	// The family order above is the diagnostic's authority: later kinds
	// overwrite earlier ones per key, so the count must mirror it.
	.droppedCrossTyped = crossTypedDropCount(
		stagedLens(.nodeColI64), stagedLens(.nodeColF64),
		stagedLens(.nodeColBool), stagedLens(.nodeColStr),
	) + crossTypedDropCount(
		stagedLens(.relColI64), stagedLens(.relColF64),
		stagedLens(.relColBool), stagedLens(.relColStr),
	)

	.atoms = .aliasAtoms()
	.carryLazyCaches()

	// Eagerly build the requested equality indexes from the finished
	// columns -- identical to what the lazy path would build on demand, and
	// skipped for an index already carried forward from the source.
	for ,  := range  {
		,  := .atoms.ID()
		if ! {
			continue
		}
		,  := .columns[]
		if ! {
			continue
		}
		for ,  := range .labelIndex {
			 := propIndexKey{label: , key: }
			if ,  := .propIndex[];  {
				continue
			}
			.propIndex[] = buildPropValueIndex(, )
		}
	}
	return 
}