package jsonld

import (
	"encoding/json"
	"strings"
)

// From the JSON-LD spec 3.3
// https://www.w3.org/TR/json-ld/#dfn-keyword
const (
	// @context
	// Used to define the short-hand names that are used throughout a JSON-LD document.
	// These short-hand names are called terms and help developers to express specific identifiers in a compact manner.
	// The @context keyword is described in detail in section 5.1 The Context.
	ContextKw Term = "@context"
	// @id
	//Used to uniquely identify things that are being described in the document with IRIs or blank node identifiers.
	// This keyword is described in section 5.3 Node Identifiers.
	IdKw Term = "@id"
	// @value
	// Used to specify the data that is associated with a particular property in the graph.
	// This keyword is described in section 6.9 String Internationalization and section 6.4 Typed Values.
	ValueKw Term = "@value"
	// @language
	// Used to specify the language for a particular string value or the default language of a JSON-LD document.
	// This keyword is described in section 6.9 String Internationalization.
	LanguageKw Term = "@language"
	//@type
	//Used to set the data type of a node or typed value. This keyword is described in section 6.4 Typed Values.
	TypeKw Term = "@type"
	// @container
	// Used to set the default container type for a term. This keyword is described in section 6.11 Sets and Lists.
	ContainerKw Term = "@container"
	//@list
	//Used to express an ordered set of data. This keyword is described in section 6.11 Sets and Lists.
	ListKw Term = "@list"
	// @set
	// Used to express an unordered set of data and to ensure that values are always represented as arrays.
	// This keyword is described in section 6.11 Sets and Lists.
	SetKw Term = "@set"
	// @reverse
	// Used to express reverse properties. This keyword is described in section 6.12 Reverse Properties.
	ReverseKw Term = "@reverse"
	// @index
	// Used to specify that a container is used to index information and that processing should continue deeper
	// into a JSON data structure. This keyword is described in section 6.16 Data Indexing.
	IndexKw Term = "@index"
	// @base
	// Used to set the base IRI against which relative IRIs are resolved. T
	// his keyword is described in section 6.1 Base IRI.
	BaseKw Term = "@base"
	// @vocab
	// Used to expand properties and values in @type with a common prefix IRI.
	// This keyword is described in section 6.2 Default Vocabulary.
	VocabKw Term = "@vocab"
	// @graph
	// Used to express a graph. This keyword is described in section 6.13 Named Graphs.
	GraphKw Term = "@graph"
)

// ContentType is the content type of JsonLD documents
const ContentType = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`

type (
	// Ref basic type
	LangRef string
	// Term represents the JSON-LD term for @context maps
	Term string
	// IRI is a International Resource Identificator
	IRI string
	// Terms is an array of Term values
	Terms []Term
)

// Nillable
type Nillable interface {
	IsNil() bool
}

type IRILike interface {
	IsCompact() bool
	IsAbsolute() bool
	IsRelative() bool
}

func (i IRI) IsCompact() bool {
	return !i.IsAbsolute() && strings.Contains(string(i), ":")
}
func (i IRI) IsAbsolute() bool {
	return strings.Contains(string(i), "https://")
}
func (i IRI) IsRelative() bool {
	return !i.IsAbsolute()
}

var keywords = Terms{
	BaseKw,
	ContextKw,
	ContainerKw,
	GraphKw,
	IdKw,
	IndexKw,
	LanguageKw,
	ListKw,
	ReverseKw,
	SetKw,
	TypeKw,
	ValueKw,
	VocabKw,
}

const NilTerm Term = "-"
const NilLangRef LangRef = "-"

type ContextObject struct {
	ID   interface{} `jsonld:"@id,omitempty,collapsible"`
	Type interface{} `jsonld:"@type,omitempty,collapsible"`
}

// Context is of of the basic JSON-LD elements.
// It represents an array of ContextElements
type Context []ContextElement

// ContextElement is used to map terms to IRIs or JSON objects.
// Terms are case sensitive and any valid string that is not a reserved JSON-LD
// keyword can be used as a term.
type ContextElement struct {
	Term Term
	IRI  IRI
}

func GetContext() Context {
	return Context{}
}

//type Context Collapsible

// Collapsible is an interface used by the JSON-LD marshaller to collapse a struct to one single value
type Collapsible interface {
	Collapse() interface{}
}

// Collapse returns the plain text collapsed value of the current Context object
func (c Context) Collapse() interface{} {
	if len(c) == 1 && len(c[0].IRI) > 0 {
		return c[0].IRI
	}
	for _, el := range c {
		if el.Term == NilTerm {

		}
	}

	return c
}

// Collapse returns the plain text collapsed value of the current IRI string
func (i IRI) Collapse() interface{} {
	return i
}

// MarshalText basic stringify function
func (i IRI) MarshalText() ([]byte, error) {
	return []byte(i), nil
}

// MarshalJSON returns the JSON document represented by the current Context
// This should return :
// If only one element in the context and the element has no Term -> json marshaled string
// If multiple elements in the context without Term -> json marshaled array of strings
// If multiple elements where at least one doesn't have a Term and one has a Term -> json marshaled array
// If multiple elements where all have Terms -> json marshaled object
func (c Context) MarshalJSON() ([]byte, error) {
	mapIRI := make(map[Term]IRI, 0)
	arr := make([]interface{}, 0)
	i := 0
	if len(c) == 1 && len(c[0].IRI) > 0 {
		return json.Marshal(c[0].IRI)
	}
	for _, el := range c {
		t := el.Term
		iri := el.IRI
		if t.IsNil() {
			arr = append(arr, iri)
			i += 1
		} else {
			if len(iri) > 0 {
				mapIRI[t] = iri
			}
		}
	}
	if len(mapIRI) > 0 {
		if len(arr) == 0 {
			return json.Marshal(mapIRI)
		}
		arr = append(arr, mapIRI)
	}
	return json.Marshal(arr)
}

// UnmarshalJSON tries to load the Context from the incoming json value
func (c *Context) UnmarshalJSON(data []byte) error {
	return nil
}

// IsNil returns if current LangRef is equal to empty string or to its nil value
func (l LangRef) IsNil() bool {
	return len(l) == 0 || l == NilLangRef
}

// IsNil returns if current IRI is equal to empty string
func (i IRI) IsNil() bool {
	return len(i) == 0
}

// IsNil returns if current Term is equal to empty string or to its nil value
func (i Term) IsNil() bool {
	return len(i) == 0 || i == NilTerm
}
