@prefix : <https://ontology.inferal.com/modules/localization/> .
@prefix geo: <http://www.opengis.net/ont/geosparql#> .
@prefix ord: <https://ontology.inferal.com/modules/ordering/> .
@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix rng: <https://ontology.inferal.com/modules/range/> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

:FrameShape
    a sh:NodeShape ;
    rdfs:label "frame shape" ;
    rdfs:comment "Validates localization frame declarations, optional ordered-frame backing, and datatype registry entries." ;
    sh:targetClass :Frame ;
    sh:property [
        sh:path :orderedBy ;
        sh:class ord:Ordering ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :frameOfDatatype ;
        sh:nodeKind sh:IRI ;
    ] .

:LocusShape
    a sh:NodeShape ;
    rdfs:label "locus shape" ;
    rdfs:comment "Validates frame membership, shorthand position cardinality, and frame resolvability for literal position values." ;
    sh:targetClass :Locus ;
    sh:property [
        sh:path :inFrame ;
        sh:class :Frame ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :at ;
        sh:maxCount 1 ;
    ] ;
    sh:sparql [
        sh:message "A Locus without loc:at must declare loc:inFrame so the stable position subject has a reference frame." ;
        sh:select """
            PREFIX loc: <https://ontology.inferal.com/modules/localization/>
            SELECT $this WHERE {
                FILTER NOT EXISTS { $this loc:inFrame ?frame . }
                FILTER NOT EXISTS { $this loc:at ?value . }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "For an ordered frame, a range-valued loc:at must use rng:Range bounds that are loci in the same frame." ;
        sh:select """
            PREFIX loc: <https://ontology.inferal.com/modules/localization/>
            PREFIX rng: <https://ontology.inferal.com/modules/range/>
            SELECT $this ?range ?bound WHERE {
                $this loc:inFrame ?frame ;
                      loc:at ?range .
                ?frame loc:orderedBy ?ordering .
                ?range a rng:Range .
                {
                    ?range rng:lowerBound ?bound .
                }
                UNION
                {
                    ?range rng:upperBound ?bound .
                }
                FILTER NOT EXISTS {
                    ?bound a loc:Locus ;
                           loc:inFrame ?frame .
                }
            }
        """ ;
    ] .

:UnresolvedLiteralWarningShape
    a sh:NodeShape ;
    rdfs:label "unresolved literal warning shape" ;
    rdfs:comment "Warns when a locus shorthand literal has no explicit frame and cannot be resolved by embedded CRS, property default, or datatype registry." ;
    sh:targetClass :Locus ;
    sh:severity sh:Warning ;
    sh:sparql [
        sh:message "A literal loc:at value without explicit loc:inFrame should resolve by embedded CRS, loc:defaultFrame, or loc:frameOfDatatype; xsd:dateTime is a floating time unless the property supplies a default frame." ;
        sh:select """
            PREFIX geo: <http://www.opengis.net/ont/geosparql#>
            PREFIX loc: <https://ontology.inferal.com/modules/localization/>
            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
            SELECT $this ?value WHERE {
                $this loc:at ?value .
                FILTER(isLiteral(?value))
                FILTER NOT EXISTS { $this loc:inFrame ?frame . }
                BIND(DATATYPE(?value) AS ?datatype)
                FILTER NOT EXISTS { loc:at loc:defaultFrame ?propertyFrame . }
                FILTER NOT EXISTS { ?datatypeFrame loc:frameOfDatatype ?datatype . }
                FILTER(
                    ?datatype != geo:wktLiteral ||
                    !REGEX(STR(?value), "^\\\\s*<[^>]+>")
                )
            }
        """ ;
    ] .

:FloatingDateTimeWarningShape
    a sh:NodeShape ;
    rdfs:label "floating dateTime warning shape" ;
    rdfs:comment "Warns when a timezone-less xsd:dateTime value is used without an explicit or property-default frame." ;
    sh:targetClass :Locus ;
    sh:severity sh:Warning ;
    sh:sparql [
        sh:message "A floating xsd:dateTime loc:at value does not resolve to the UTC timeline unless loc:inFrame or a property default frame is explicit." ;
        sh:select """
            PREFIX loc: <https://ontology.inferal.com/modules/localization/>
            PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
            SELECT $this ?value WHERE {
                $this loc:at ?value .
                FILTER(isLiteral(?value))
                FILTER(DATATYPE(?value) = xsd:dateTime)
                FILTER NOT EXISTS { $this loc:inFrame ?frame . }
                FILTER NOT EXISTS { loc:at loc:defaultFrame ?propertyFrame . }
            }
        """ ;
    ] .

:LocalizationShape
    a sh:NodeShape ;
    rdfs:label "localization shape" ;
    rdfs:comment "Validates that each localization is one claim about one locus with exactly one position value." ;
    sh:targetClass :Localization ;
    sh:property [
        sh:path :localizes ;
        sh:class :Locus ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :at ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:sparql [
        sh:message "For an ordered frame, a range-valued localization must use rng:Range bounds that are loci in the same frame as the localized locus." ;
        sh:select """
            PREFIX loc: <https://ontology.inferal.com/modules/localization/>
            PREFIX rng: <https://ontology.inferal.com/modules/range/>
            SELECT $this ?range ?bound WHERE {
                $this loc:localizes ?locus ;
                      loc:at ?range .
                ?locus loc:inFrame ?frame .
                ?frame loc:orderedBy ?ordering .
                ?range a rng:Range .
                {
                    ?range rng:lowerBound ?bound .
                }
                UNION
                {
                    ?range rng:upperBound ?bound .
                }
                FILTER NOT EXISTS {
                    ?bound a loc:Locus ;
                           loc:inFrame ?frame .
                }
            }
        """ ;
    ] .

:DefaultFramePropertyShape
    a sh:NodeShape ;
    rdfs:label "default frame property shape" ;
    rdfs:comment "Validates property-level default frame annotations used by the literal frame resolution rule." ;
    sh:targetSubjectsOf :defaultFrame ;
    sh:property [
        sh:path :defaultFrame ;
        sh:class :Frame ;
        sh:minCount 1 ;
    ] .
