// Plain-data model of a serialized graph snapshot and the per-section
// codecs for atoms, meta, nodes (label index), and relationships (CSR +
// type index). Port of the Rust codec's graph.rs plus the section
// encoders/decoders from rcpg.rs.

package rcpg

import (
	
	

	
)

// BitmapEntry is one entry of a bitmap index: an atom mapped to a roaring
// bitmap (label atom -> node IDs, or type atom -> outgoing-CSR positions).
type BitmapEntry struct {
	Atom   uint32
	Bitmap *roaring.Bitmap
}

// Column is one property column: a key atom and its storage.
type Column struct {
	Key  uint32
	Data ColumnData
}

// GraphSection is the complete decoded content of an RCPG file.
//
// All string-ish values (labels, relationship types, property keys, string
// property values) are atom IDs into Atoms; atom 0 is always the empty
// string. A nil Version means the meta section carried no version string
// (present-but-empty is distinct on disk).
type GraphSection struct {
	// NNodes is the actual number of distinct nodes, NOT the CSR ID-space
	// size; see OutOffsets.
	NNodes uint32
	// NRels is the number of relationships.
	NRels uint64

	// OutOffsets has csr_id_space + 1 entries. IDs never added have empty
	// ranges, so the ID space can exceed NNodes under sparse IDs.
	OutOffsets []uint32
	// OutNbrs holds destination node IDs, len = NRels.
	OutNbrs []uint32
	// OutTypes holds relationship type atoms, parallel to OutNbrs.
	OutTypes []uint32
	// InOffsets is sized like OutOffsets.
	InOffsets []uint32
	// InNbrs holds source node IDs, len = NRels.
	InNbrs []uint32
	// InTypes holds relationship type atoms, parallel to InNbrs.
	InTypes []uint32

	// LabelIndex maps label atom -> node IDs, sorted by label atom.
	LabelIndex []BitmapEntry
	// TypeIndex maps type atom -> outgoing-CSR relationship positions,
	// sorted by type atom.
	TypeIndex []BitmapEntry

	// NodeColumns are node property columns, sorted by key atom.
	NodeColumns []Column
	// RelColumns are relationship property columns, indexed by the
	// relationship's outgoing-CSR position.
	RelColumns []Column

	// Version is the optional snapshot version string.
	Version *string

	// Atoms is the string table; index 0 is always "".
	Atoms []string

	// Existence is the optional section-8 node existence bitmap: the set
	// of ids that are actual nodes, for id spaces with gaps (builder-
	// chosen sparse ids). Nil means absent, in which case every
	// in-id-space id is a node -- exactly true for dense writers, whose
	// files stay byte-identical by never emitting the section.
	Existence *roaring.Bitmap

	// AtomIndex is the optional section-7 block index over the atoms
	// section, present only when the file carried one: lazy readers route
	// an atom id straight to its block instead of scanning the section's
	// length prefixes. Nil on a hand-built graph; the writer derives it
	// from the atoms at encode time, so it carries no independent
	// information.
	AtomIndex *AtomBlockIndex
}

// AtomBlockIndex is a parsed section-7 body: Offsets[k] is the byte offset,
// relative to the atoms section body start, of atom k*BlockLen's length
// prefix (entry 0 is always 4, past the section's count word).
type AtomBlockIndex struct {
	BlockLen uint32
	Offsets  []uint64
}

// CSRIDSpace is the size of the CSR ID space (max node ID + 1 for non-empty
// graphs).
func ( *GraphSection) () uint32 {
	if len(.OutOffsets) == 0 {
		return 0
	}
	return uint32(len(.OutOffsets) - 1)
}

// OutNeighbors returns the outgoing neighbors of node, empty for IDs outside
// the ID space.
func ( *GraphSection) ( uint32) []uint32 {
	return neighbors(.OutOffsets, .OutNbrs, )
}

// InNeighbors returns the incoming neighbors (source node IDs) of node,
// empty for IDs outside the ID space.
func ( *GraphSection) ( uint32) []uint32 {
	return neighbors(.InOffsets, .InNbrs, )
}

func neighbors(,  []uint32,  uint32) []uint32 {
	 := int()
	if +1 >= len() {
		return nil
	}
	,  := int([]), int([+1])
	if  >  ||  > len() {
		return nil
	}
	return [:]
}

// preallocCap bounds an initial slice reservation by a file-declared element
// count: each element is validated as the decode loop reads it, but the
// up-front reservation happens first, so a crafted count could otherwise
// reserve gigabytes before the loop rejects the truncation.
func preallocCap( int) int {
	const  = 4096
	return min(, )
}

// --- atoms -------------------------------------------------------------------

func encodeAtoms( []string) ([]byte, error) {
	var  bytes.Buffer
	if  := wLenPrefix(&, len());  != nil {
		return nil, 
	}
	for ,  := range  {
		if  := wString(&, );  != nil {
			return nil, 
		}
	}
	return .Bytes(), nil
}

func decodeAtoms( []byte) ([]string, error) {
	 := newCursor()
	,  := .lenPrefix(1)
	if  != nil {
		return nil, 
	}
	// One backing string serves every atom: substrings share it, so the
	// table costs two allocations instead of one per atom (the dominant
	// residual of the load path at millions of atoms). The backing spans
	// the section tail, so the interleaved length prefixes ride along --
	// a few bytes per atom inside one block that stays alive with the
	// table anyway.
	 := .pos
	 := string(.b[:])
	 := make([]string, 0, preallocCap())
	for range  {
		,  := .lenPrefix(1)
		if  != nil {
			return nil, 
		}
		,  := .take()
		if  != nil {
			return nil, 
		}
		if !utf8.Valid() {
			return nil, corruptf("invalid utf8 at offset %d", .pos)
		}
		 = append(, [.pos--:.pos-])
	}
	return , nil
}

// --- atom index (section 7) ----------------------------------------------

// encodeAtomIndex encodes the optional block index over the atoms section:
// block_len u32 + reserved u32 + count u32 + count x u64 offsets, where
// offsets[k] is the atoms-section-body-relative offset of atom
// k*blockLen's length prefix (entry 0 is always 4, past the count word).
func encodeAtomIndex( []string,  uint32) ([]byte, error) {
	var  bytes.Buffer
	wU32(&, )
	wU32(&, 0) // reserved
	 := (len() + int() - 1) / int()
	wU32(&, uint32())
	 := uint64(4) // past the atoms section's count word
	for ,  := range  {
		if %int() == 0 {
			wU64(&, )
		}
		 += 4 + uint64(len())
	}
	return .Bytes(), nil
}

func decodeAtomIndex( []byte) (*AtomBlockIndex, error) {
	 := newCursor()
	,  := .u32()
	if  != nil {
		return nil, 
	}
	if ,  := .u32();  != nil { // reserved
		return nil, 
	}
	,  := .lenPrefix(8)
	if  != nil {
		return nil, 
	}
	 := make([]uint64, 0, preallocCap())
	 := uint64(0)
	 := false
	for range  {
		,  := .u64()
		if  != nil {
			return nil, 
		}
		// Strictly ascending by construction; enforce here so a consumer
		// can trust it.
		if  &&  <=  {
			return nil, corruptf("atom index: offsets not strictly ascending")
		}
		,  = , true
		 = append(, )
	}
	return &AtomBlockIndex{BlockLen: , Offsets: }, nil
}

// validateAtomIndex checks a PRESENT atom index against the decoded atoms:
// a malformed index is corrupt, never a silent fall-back to the scan path
// (a lazy remote reader falling back would re-transfer the whole atoms
// section). Files without the section skip this entirely.
func validateAtomIndex( *AtomBlockIndex,  []string) error {
	if .BlockLen == 0 {
		return corruptf("atom index: block_len is zero")
	}
	 := (len() + int(.BlockLen) - 1) / int(.BlockLen)
	if len(.Offsets) !=  {
		return corruptf("atom index: offset count does not match the atom count")
	}
	// Recompute each block-start offset from the atoms themselves: 4
	// (count word) + 4 per preceding length prefix + preceding UTF-8
	// bytes.
	 := uint64(4)
	 := 0
	for ,  := range  {
		if %int(.BlockLen) == 0 {
			if .Offsets[] !=  {
				return corruptf("atom index: offset does not point at its block's length prefix")
			}
			++
		}
		 += 4 + uint64(len())
	}
	return nil
}

// --- meta --------------------------------------------------------------------

func encodeMeta( *GraphSection) ([]byte, error) {
	var  bytes.Buffer
	if .Version != nil {
		wU8(&, 1)
		if  := wString(&, *.Version);  != nil {
			return nil, 
		}
	} else {
		wU8(&, 0)
	}
	return .Bytes(), nil
}

func decodeMeta( []byte,  *GraphSection) error {
	 := newCursor()
	,  := .u8()
	if  != nil {
		return 
	}
	if  == 1 {
		,  := .str()
		if  != nil {
			return 
		}
		.Version = &
	}
	return nil
}

// --- nodes (label index) -------------------------------------------------------

func encodeNodes( *GraphSection) ([]byte, error) {
	var  bytes.Buffer
	wU32(&, .NNodes)
	if  := encodeBitmapIndex(&, .LabelIndex);  != nil {
		return nil, 
	}
	return .Bytes(), nil
}

func decodeNodes( []byte,  *GraphSection) error {
	 := newCursor()
	,  := .u32()
	if  != nil {
		return 
	}
	.NNodes = 
	.LabelIndex,  = decodeBitmapIndex()
	return 
}

// --- relationships (CSR + type index) --------------------------------------------

func encodeRels( *GraphSection) ([]byte, error) {
	var  bytes.Buffer
	wU64(&, .NRels)
	for ,  := range [][]uint32{
		.OutOffsets, .OutNbrs, .OutTypes, .InOffsets, .InNbrs, .InTypes,
	} {
		if  := wU32Vec(&, );  != nil {
			return nil, 
		}
	}
	if  := encodeBitmapIndex(&, .TypeIndex);  != nil {
		return nil, 
	}
	return .Bytes(), nil
}

func decodeRels( []byte,  *GraphSection) error {
	 := newCursor()
	var  error
	if .NRels,  = .u64();  != nil {
		return 
	}
	for ,  := range []*[]uint32{
		&.OutOffsets, &.OutNbrs, &.OutTypes, &.InOffsets, &.InNbrs, &.InTypes,
	} {
		if *,  = .u32Vec();  != nil {
			return 
		}
	}
	.TypeIndex,  = decodeBitmapIndex()
	return 
}

// --- existence (section 8: one portable roaring bitmap) -----------------------------

// encodeExistence writes the node existence bitmap as one portable
// roaring serialization (the section body is exactly the bitmap; the
// directory carries the length).
func encodeExistence( *roaring.Bitmap) ([]byte, error) {
	var  bytes.Buffer
	if ,  := .WriteTo(&);  != nil {
		return nil, 
	}
	return .Bytes(), nil
}

// decodeExistence parses a section-8 body back into the bitmap.
func decodeExistence( []byte) (*roaring.Bitmap, error) {
	 := roaring.New()
	if ,  := .ReadFrom(bytes.NewReader());  != nil {
		return nil, corruptf("invalid existence bitmap: %v", )
	}
	return , nil
}

// --- bitmap indexes (label/type -> roaring) ----------------------------------------

// Bitmaps use the portable RoaringFormatSpec serialization, interoperable
// with the Rust and C implementations. Go roaring's WriteTo/ReadFrom speak
// exactly that format; the Go-specific "frozen" form is never used.
func encodeBitmapIndex( *bytes.Buffer,  []BitmapEntry) error {
	if  := wLenPrefix(, len());  != nil {
		return 
	}
	for ,  := range  {
		wU32(, .Atom)
		 := .Bitmap.GetSerializedSizeInBytes()
		if  := wLenPrefix(, int());  != nil {
			return 
		}
		if ,  := .Bitmap.WriteTo();  != nil {
			return 
		}
	}
	return nil
}

func decodeBitmapIndex( *cursor) ([]BitmapEntry, error) {
	,  := .lenPrefix(8)
	if  != nil {
		return nil, 
	}
	 := make([]BitmapEntry, 0, preallocCap())
	for range  {
		,  := .u32()
		if  != nil {
			return nil, 
		}
		,  := .lenPrefix(1)
		if  != nil {
			return nil, 
		}
		,  := .take()
		if  != nil {
			return nil, 
		}
		 := roaring.New()
		if ,  := .ReadFrom(bytes.NewReader());  != nil {
			return nil, corruptf("invalid roaring bitmap: %v", )
		}
		 = append(, BitmapEntry{Atom: , Bitmap: })
	}
	return , nil
}