// N-Quads interchange for Snapshots (task 031): the RDF sibling of the
// RCPG functions in serialize.go. WriteNQuads serializes a snapshot as a
// deterministic N-Quads document under the urn:chickpeas: vocabulary;
// ReadNQuads maps any N-Quads/N-Triples document -- ours or foreign --
// into a Snapshot. Parsing and term escaping are github.com/freeeve/
// libcodex/rdf.
//
// Write vocabulary:
//   - node subject: urn:chickpeas:node/<id> (decimal; sparse ids survive)
//   - label: <node> rdf:type urn:chickpeas:label/<enc(name)>
//   - node prop: <node> urn:chickpeas:prop/<enc(key)> <literal>
//   - rel: <u> urn:chickpeas:rel/<enc(type)> <v> urn:chickpeas:relid/<pos>
//     -- the graph term is the rel's identity, so parallel rels stay
//     distinct statements and rel properties have an anchor
//   - rel prop: <relid> urn:chickpeas:prop/<enc(key)> <literal>
//   - version: <urn:chickpeas:graph> <urn:chickpeas:version> "<v>"
//
// Read mapping (generic, with the chickpeas vocabulary as a special
// spelling of each rule):
//   - rdf:type with an IRI object labels the subject (chickpeas label
//     prefix decoded, otherwise the local name after '#'/'/');
//   - a literal object becomes a property of the subject, first value
//     wins, typed from the xsd datatype;
//   - a resource object becomes a rel typed by the predicate's local
//     or decoded name;
//   - a non-default graph term tagging exactly one rel quad, never used
//     as a rel/label subject or object, anchors that rel: literal
//     statements about it attach as rel properties (the named-graph-
//     per-edge pattern);
//   - urn:chickpeas:node/<n> terms keep id n; other entities intern
//     sequentially in document order, and foreign IRI entities carry a
//     `uri` property (blank nodes don't).
//
// A bare node (no labels, props, or rels) appears in no quad and does
// not survive a write/read cycle; RCPG remains the fidelity format.

package chickpeas

import (
	
	
	
	
	
	
	
	
	

	
)

const (
	nqNodePrefix  = "urn:chickpeas:node/"
	nqLabelPrefix = "urn:chickpeas:label/"
	nqPropPrefix  = "urn:chickpeas:prop/"
	nqRelPrefix   = "urn:chickpeas:rel/"
	nqRelIDPrefix = "urn:chickpeas:relid/"
	nqGraphIRI    = "urn:chickpeas:graph"
	nqVersionIRI  = "urn:chickpeas:version"

	xsdInteger = "http://www.w3.org/2001/XMLSchema#integer"
	xsdDouble  = "http://www.w3.org/2001/XMLSchema#double"
	xsdBoolean = "http://www.w3.org/2001/XMLSchema#boolean"

	nqFlushSize = 1 << 20
)

// nqUnescape decodes %XX sequences; malformed sequences pass through
// verbatim.
func nqUnescape( string) string {
	if !strings.ContainsRune(, '%') {
		return 
	}
	 := make([]byte, 0, len())
	for  := 0;  < len(); {
		if [] == '%' && +2 < len() {
			if ,  := nqHexVal([+1]), nqHexVal([+2]);  >= 0 &&  >= 0 {
				 = append(, byte(<<4|))
				 += 3
				continue
			}
		}
		 = append(, [])
		++
	}
	return string()
}

func nqHexVal( byte) int {
	switch {
	case  >= '0' &&  <= '9':
		return int( - '0')
	case  >= 'a' &&  <= 'f':
		return int(-'a') + 10
	case  >= 'A' &&  <= 'F':
		return int(-'A') + 10
	}
	return -1
}

// ReadNQuadsFile reads an N-Quads/N-Triples file (transparently
// gunzipping) into a Snapshot via ReadNQuads.
func ( string) (*Snapshot, error) {
	,  := os.ReadFile()
	if  != nil {
		return nil, 
	}
	return ReadNQuads()
}

// ReadNQuads builds a Snapshot from an N-Quads or N-Triples document
// using the read mapping documented at the top of this file. Gzipped
// input (the 1f 8b magic) is decompressed transparently.
func ( []byte) (*Snapshot, error) {
	if len() >= 2 && [0] == 0x1f && [1] == 0x8b {
		,  := gzip.NewReader(bytes.NewReader())
		if  != nil {
			return nil, 
		}
		,  := io.ReadAll()
		if  != nil {
			return nil, 
		}
		if  := .Close();  != nil {
			return nil, 
		}
		 = 
	}
	,  := rdf.ParseNQuadsShared()
	if  != nil {
		return nil, 
	}
	return buildFromQuads(.Quads)
}

// Quad classification for buildFromQuads.
const (
	nqQSkip uint8 = iota
	nqQLabel
	nqQProp
	nqQRel
)

// buildFromQuads maps parsed quads into a Snapshot per the read mapping.
func buildFromQuads( []rdf.Quad) (*Snapshot, error) {
	// Pass 1: classify each quad, count rel quads per named graph term,
	// and note which terms act as rel/label subjects or objects (those
	// can never anchor rel properties).
	 := make([]uint8, len())
	 := map[rdf.Term]int{}
	 := map[rdf.Term]bool{}
	var  string
	var  bool
	for  := range  {
		 := &[]
		switch {
		case .S.Kind == rdf.IRI && .S.Value == nqGraphIRI &&
			.P.Value == nqVersionIRI && .O.Kind == rdf.Literal:
			[] = nqQSkip
			,  = .O.Value, true
		case .P.Value == rdf.TypeIRI && .O.Kind == rdf.IRI:
			[] = nqQLabel
			[.S] = true
		case .O.Kind == rdf.Literal:
			[] = nqQProp
		default:
			[] = nqQRel
			[.S] = true
			[.O] = true
			if .G != (rdf.Term{}) {
				[.G]++
			}
		}
	}
	 := map[rdf.Term]bool{}
	for ,  := range  {
		if  == 1 && ![] {
			[] = true
		}
	}

	// Pass 2: assign node ids. Native urn:chickpeas:node/<n> terms claim
	// their id first (spelling decides, independent of position); the
	// rest intern sequentially in document order, skipping claimed ids.
	 := map[rdf.Term]NodeID{}
	 := map[NodeID]bool{}
	 := func( func( rdf.Term)) {
		for  := range  {
			 := &[]
			switch [] {
			case nqQLabel:
				(.S)
			case nqQProp:
				if ![.S] {
					(.S)
				}
			case nqQRel:
				(.S)
				(.O)
			}
		}
	}
	(func( rdf.Term) {
		if ,  := [];  {
			return
		}
		if ,  := nqNativeID();  {
			[] = 
			[] = true
		}
	})
	var  NodeID
	(func( rdf.Term) {
		if ,  := [];  {
			return
		}
		for [] {
			++
		}
		[] = 
		[] = true
	})

	// Pass 3: collect labels, then create nodes in ascending id order.
	// Foreign IRI entities carry their IRI as a `uri` property.
	 := map[NodeID]map[string]bool{}
	for  := range  {
		if [] != nqQLabel {
			continue
		}
		 := &[]
		 := [.S]
		if [] == nil {
			[] = map[string]bool{}
		}
		[][nqDecodeName(.O.Value, nqLabelPrefix)] = true
	}
	 := map[NodeID]string{}
	for ,  := range  {
		if .Kind == rdf.IRI {
			if ,  := nqNativeID(); ! {
				[] = .Value
			}
		}
	}
	 := make([]NodeID, 0, len())
	for  := range  {
		 = append(, )
	}
	slices.Sort()
	 := NewBuilder(len(), len())
	 := map[uint64]bool{}
	 := .InternPropertyKey("uri")
	for ,  := range  {
		 := make([]string, 0, len([]))
		for  := range [] {
			 = append(, )
		}
		sort.Strings()
		if ,  := .AddNodeWithID(, ...);  != nil {
			return nil, 
		}
		if ,  := [];  {
			if  := .SetPropByKey(, , );  != nil {
				return nil, 
			}
			[uint64()<<32|uint64()] = true
		}
	}

	// Pass 4: rels in document order, remembering which builder index
	// each anchoring graph term produced.
	 := map[rdf.Term]int{}
	for  := range  {
		if [] != nqQRel {
			continue
		}
		 := &[]
		,  := .AddRel([.S], [.O], nqDecodeName(.P.Value, nqRelPrefix))
		if  != nil {
			return nil, 
		}
		if [.G] {
			[.G] = 
		}
	}

	// Pass 5: properties in document order, first value wins per
	// (entity, key); anchor subjects address their rel instead of a node.
	 := map[uint64]bool{}
	for  := range  {
		if [] != nqQProp {
			continue
		}
		 := &[]
		 := .InternPropertyKey(nqDecodeName(.P.Value, nqPropPrefix))
		 := nqLiteralValue(.O)
		if [.S] {
			 := [.S]
			 := uint64()<<32 | uint64()
			if [] {
				continue
			}
			[] = true
			if  := .SetRelPropAt(, nqDecodeName(.P.Value, nqPropPrefix), );  != nil {
				return nil, 
			}
			continue
		}
		 := [.S]
		 := uint64()<<32 | uint64()
		if [] {
			continue
		}
		[] = true
		if  := .SetPropByKey(, , );  != nil {
			return nil, 
		}
	}

	if  {
		.SetVersion()
	}
	return .Finalize(), nil
}

// nqNativeID matches urn:chickpeas:node/<decimal> and returns the id.
func nqNativeID( rdf.Term) (NodeID, bool) {
	if .Kind != rdf.IRI || !strings.HasPrefix(.Value, nqNodePrefix) {
		return 0, false
	}
	,  := strconv.ParseUint(.Value[len(nqNodePrefix):], 10, 32)
	if  != nil {
		return 0, false
	}
	return NodeID(), true
}

// nqDecodeName maps an IRI to a label/key/type name: the unescaped
// remainder for the given chickpeas prefix, the local name otherwise.
func nqDecodeName(,  string) string {
	if strings.HasPrefix(, ) {
		return nqUnescape([len():])
	}
	return nqLocalName()
}

// nqLocalName is the substring after the last '#', then after the last
// '/' of that remainder (the SPB loader's local-name rule).
func nqLocalName( string) string {
	if  := strings.LastIndexByte(, '#');  >= 0 {
		 = [+1:]
	}
	if  := strings.LastIndexByte(, '/');  >= 0 {
		 = [+1:]
	}
	return 
}

// nqLiteralValue maps a literal to the most specific property type its
// xsd datatype allows, falling back to the lexical string.
func nqLiteralValue( rdf.Term) any {
	switch nqLocalName(.Datatype) {
	case "integer", "int", "long", "short", "byte",
		"nonNegativeInteger", "positiveInteger",
		"nonPositiveInteger", "negativeInteger",
		"unsignedLong", "unsignedInt", "unsignedShort", "unsignedByte":
		if ,  := strconv.ParseInt(.Value, 10, 64);  == nil {
			return 
		}
	case "double", "float", "decimal":
		switch .Value {
		case "INF", "+INF":
			return math.Inf(1)
		case "-INF":
			return math.Inf(-1)
		case "NaN":
			return math.NaN()
		}
		if ,  := strconv.ParseFloat(.Value, 64);  == nil {
			return 
		}
	case "boolean":
		switch .Value {
		case "true":
			return true
		case "false":
			return false
		}
	}
	return .Value
}