package rdf

import (
	
	
	
	
)

// ParseJSONLD parses a JSON-LD document into a Graph. It expands compact IRIs and
// terms against an inline @context (prefix maps and term definitions, including
// "@type":"@id" coercion), and handles @graph, @id, @type, @value/@language/@type
// literals, @list, nested node objects, and arrays of any of these. It does not
// fetch remote @context documents or run full JSON-LD 1.1 framing.
func ( []byte) (*Graph, error) {
	var  any
	if  := json.Unmarshal(, &);  != nil {
		return nil, 
	}
	 := &jsonldParser{g: &Graph{}, ctx: map[string]string{}, coerce: map[string]string{}, expanded: map[string]string{}}

	// A document is a node, an array of nodes, or an object wrapping @graph.
	switch d := .(type) {
	case map[string]any:
		.loadContext(["@context"])
		if ,  := ["@graph"];  {
			.eachNode()
		} else {
			.node()
		}
	case []any:
		for ,  := range  {
			.node()
		}
	}
	return .g, nil
}

type jsonldParser struct {
	g        *Graph
	ctx      map[string]string // prefix or term -> IRI
	coerce   map[string]string // term -> "@id" (value is an IRI reference, not a literal)
	expanded map[string]string // compact IRI/term -> interned full IRI
	blanks   int
}

func ( *jsonldParser) () Term {
	.blanks++
	return NewBlank("j" + strconv.Itoa(.blanks))
}

// loadContext records prefix and term mappings from an inline @context. String
// (remote) contexts are skipped.
func ( *jsonldParser) ( any) {
	switch v := .(type) {
	case []any:
		for ,  := range  {
			.()
		}
	case map[string]any:
		for ,  := range  {
			if strings.HasPrefix(, "@") {
				continue
			}
			switch t := .(type) {
			case string:
				.ctx[] = 
			case map[string]any:
				if ,  := ["@id"].(string);  {
					.ctx[] = 
				}
				if ,  := ["@type"].(string);  &&  == "@id" {
					.coerce[] = "@id"
				}
			}
		}
	}
}

// expand resolves a compact IRI or term to a full IRI.
func ( *jsonldParser) ( string) string {
	if  == "" || [0] == '@' {
		return 
	}
	// Compact IRIs and terms recur heavily (every property, every type); intern the
	// expansion so it is built only once. The cache key is the existing JSON string,
	// so the lookup itself allocates nothing.
	if ,  := .expanded[];  {
		return 
	}
	 := .expandUncached()
	.expanded[] = 
	return 
}

func ( *jsonldParser) ( string) string {
	if , ,  := strings.Cut(, ":");  {
		if !strings.HasPrefix(, "//") {
			if ,  := .ctx[];  {
				return  + 
			}
		}
		return  // already an absolute IRI
	}
	if ,  := .ctx[];  {
		return 
	}
	return 
}

func ( *jsonldParser) ( any) {
	if ,  := .([]any);  {
		for ,  := range  {
			.node()
		}
		return
	}
	.node()
}

// node emits triples for a node object and returns its subject term.
func ( *jsonldParser) ( any) Term {
	,  := .(map[string]any)
	if ! {
		return Term{}
	}
	 := .subjectOf()

	if ,  := ["@type"];  {
		for ,  := range asSlice() {
			if ,  := .(string);  {
				.g.Add(, NewIRI(TypeIRI), NewIRI(.expand()))
			}
		}
	}
	// Iterate the predicate keys in sorted order so the emitted triples (and thus
	// the blank-node numbering and serialized output) are deterministic across
	// runs, rather than following Go's randomized map order.
	 := make([]string, 0, len())
	for  := range  {
		if !strings.HasPrefix(, "@") { // @id and @type handled; other keywords carry no statement here
			 = append(, )
		}
	}
	slices.Sort()
	for ,  := range  {
		 := NewIRI(.expand())
		 := .coerce[] == "@id"
		for ,  := range .values([], ) {
			.g.Add(, , )
		}
	}
	return 
}

// values turns a JSON-LD value (or array) into the object terms it denotes,
// emitting triples for any nested nodes.
func ( *jsonldParser) ( any,  bool) []Term {
	switch t := .(type) {
	case []any:
		var  []Term
		for ,  := range  {
			 = append(, .(, )...)
		}
		return 
	case string:
		if  {
			return []Term{NewIRI(.expand())}
		}
		return []Term{NewLiteral(, "", "")}
	case bool:
		return []Term{NewLiteral(strconv.FormatBool(), "", "http://www.w3.org/2001/XMLSchema#boolean")}
	case float64:
		// A JSON number with no fractional part maps to xsd:integer; otherwise to
		// xsd:double (the JSON-LD-to-RDF number conversion).
		if  == float64(int64()) {
			return []Term{NewLiteral(strconv.FormatInt(int64(), 10), "", xsdInteger)}
		}
		return []Term{NewLiteral(formatNumber(), "", xsdDouble)}
	case map[string]any:
		return .objectValue()
	}
	return nil
}

// objectValue interprets a JSON-LD object as a literal (@value), an RDF list
// (@list), a bare reference (@id only), or a nested node.
func ( *jsonldParser) ( map[string]any) []Term {
	if ,  := ["@value"];  {
		,  := ["@language"].(string)
		,  := ["@type"].(string)
		if  != "" {
			 = .expand()
		}
		return []Term{NewLiteral(scalarString(), , )}
	}
	if ,  := ["@list"];  {
		return []Term{.list()}
	}
	if ,  := ["@id"].(string);  && onlyKeys(, "@id") {
		return []Term{NewIRI(.expand())}
	}
	return []Term{.node()}
}

// list materializes a JSON-LD @list as an RDF collection and returns its head
// (rdf:nil for the empty list).
func ( *jsonldParser) ( any) Term {
	 := asSlice()
	if len() == 0 {
		return NewIRI(NilIRI)
	}
	 := .fresh()
	 := 
	for ,  := range  {
		for ,  := range .values(, false) {
			.g.Add(, NewIRI(FirstIRI), )
		}
		if  == len()-1 {
			.g.Add(, NewIRI(RestIRI), NewIRI(NilIRI))
		} else {
			 := .fresh()
			.g.Add(, NewIRI(RestIRI), )
			 = 
		}
	}
	return 
}

func ( *jsonldParser) ( map[string]any) Term {
	if ,  := ["@id"].(string);  {
		if strings.HasPrefix(, "_:") {
			// Prefix the document label so it can never collide with a generated
			// ("j"+n) blank: an "@id":"_:j1" would otherwise merge with the first
			// anonymous node.
			return NewBlank("u" + [2:])
		}
		return NewIRI(.expand())
	}
	return .fresh()
}

func asSlice( any) []any {
	if ,  := .([]any);  {
		return 
	}
	return []any{}
}

func onlyKeys( map[string]any,  ...string) bool {
	for  := range  {
		if !slices.Contains(, ) {
			return false
		}
	}
	return true
}

func scalarString( any) string {
	switch t := .(type) {
	case string:
		return 
	case bool:
		return strconv.FormatBool()
	case float64:
		return formatNumber()
	}
	return ""
}

// formatNumber renders a JSON number without a trailing ".0" for integers.
func formatNumber( float64) string {
	if  == float64(int64()) {
		return strconv.FormatInt(int64(), 10)
	}
	return strconv.FormatFloat(, 'g', -1, 64)
}