@prefix : <https://ontology.inferal.com/modules/graphs/> .
@prefix ord: <https://ontology.inferal.com/modules/ordering/> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
@prefix xsd: <http://www.w3.org/2001/XMLSchema#> .

:GraphShape
    a sh:NodeShape ;
    rdfs:label "graph shape" ;
    rdfs:comment "Validates graph vertices, edges, edge membership occurrences, endpoint containment, and forward/reverse edge links." ;
    sh:targetClass :Graph ;
    sh:property [
        sh:path :hasVertex ;
        sh:class :Vertex ;
    ] ;
    sh:property [
        sh:path :hasEdge ;
        sh:class :Edge ;
    ] ;
    sh:property [
        sh:path :hasEdgeMembership ;
        sh:class :EdgeMembership ;
    ] ;
    sh:sparql [
        sh:message "Every endpoint vertex of a graph edge must be listed as a vertex of the same graph." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?edge ?endpoint ?vertex WHERE {
                $this :hasEdge ?edge .
                ?edge :hasEndpoint ?endpoint .
                ?endpoint :endpointVertex ?vertex .
                FILTER NOT EXISTS { $this :hasVertex ?vertex . }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "Every hasEdge assertion must be backed by an edge membership occurrence." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?edge WHERE {
                $this :hasEdge ?edge .
                FILTER NOT EXISTS {
                    ?membership :membershipGraph $this ;
                                :membershipEdge ?edge .
                }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A hasEdgeMembership link must point to a membership occurrence whose membershipGraph is the same graph." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?membership ?declaredGraph WHERE {
                $this :hasEdgeMembership ?membership .
                OPTIONAL { ?membership :membershipGraph ?declaredGraph . }
                FILTER(!BOUND(?declaredGraph) || ?declaredGraph != $this)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A hasEdge link must agree with the edge's edgeInGraph value when that value is stated." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?edge ?declaredGraph WHERE {
                $this :hasEdge ?edge .
                ?edge :edgeInGraph ?declaredGraph .
                FILTER(?declaredGraph != $this)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "An edgeInGraph assertion must be backed by a forward hasEdge assertion." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?edge WHERE {
                ?edge :edgeInGraph $this .
                FILTER NOT EXISTS { $this :hasEdge ?edge . }
            }
        """ ;
    ] .

:EdgeMembershipShape
    a sh:NodeShape ;
    rdfs:label "edge membership shape" ;
    rdfs:comment "Validates graph-edge membership occurrences and their matching graph hasEdge assertions." ;
    sh:targetClass :EdgeMembership ;
    sh:property [
        sh:path :membershipGraph ;
        sh:class :Graph ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :membershipEdge ;
        sh:class :Edge ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:sparql [
        sh:message "An edge membership occurrence must be backed by a hasEdge assertion." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?graph ?edge WHERE {
                $this :membershipGraph ?graph ;
                      :membershipEdge ?edge .
                FILTER NOT EXISTS { ?graph :hasEdge ?edge . }
            }
        """ ;
    ] .

:DirectedGraphShape
    a sh:NodeShape ;
    rdfs:label "directed graph shape" ;
    rdfs:comment "Validates that directed graphs contain directed edges." ;
    sh:targetClass :DirectedGraph ;
    sh:property [
        sh:path :hasEdge ;
        sh:class :DirectedEdge ;
    ] .

:UndirectedGraphShape
    a sh:NodeShape ;
    rdfs:label "undirected graph shape" ;
    rdfs:comment "Validates that undirected graphs contain undirected edges." ;
    sh:targetClass :UndirectedGraph ;
    sh:property [
        sh:path :hasEdge ;
        sh:class :UndirectedEdge ;
    ] .

:EdgeShape
    a sh:NodeShape ;
    rdfs:label "edge shape" ;
    rdfs:comment "Validates graph edge membership, endpoint cardinality and consistency, loop exclusion, and direct connects assertions." ;
    sh:targetClass :Edge ;
    sh:property [
        sh:path :edgeInGraph ;
        sh:class :Graph ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :hasEndpoint ;
        sh:class :Endpoint ;
        sh:minCount 2 ;
        sh:maxCount 2 ;
    ] ;
    sh:property [
        sh:path :connects ;
        sh:class :Vertex ;
    ] ;
    sh:sparql [
        sh:message "A hasEndpoint link must point to an endpoint whose endpointOf is the same edge." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?endpoint ?declaredEdge WHERE {
                $this :hasEndpoint ?endpoint .
                OPTIONAL { ?endpoint :endpointOf ?declaredEdge . }
                FILTER(!BOUND(?declaredEdge) || ?declaredEdge != $this)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "An endpointOf assertion must be backed by a forward hasEndpoint assertion." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?endpoint WHERE {
                ?endpoint :endpointOf $this .
                FILTER NOT EXISTS { $this :hasEndpoint ?endpoint . }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A base graph edge must have two distinct endpoint vertices. Loops require a later pseudograph or loop extension." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?vertex ?left ?right WHERE {
                ?left :endpointOf $this ;
                      :endpointVertex ?vertex .
                ?right :endpointOf $this ;
                       :endpointVertex ?vertex .
                FILTER(?left != ?right)
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A direct connects assertion must be backed by an endpoint for the same edge and vertex." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            SELECT $this ?vertex WHERE {
                $this :connects ?vertex .
                FILTER NOT EXISTS {
                    ?endpoint :endpointOf $this ;
                              :endpointVertex ?vertex .
                }
            }
        """ ;
    ] .

:EndpointShape
    a sh:NodeShape ;
    rdfs:label "endpoint shape" ;
    rdfs:comment "Validates that endpoints identify exactly one edge and exactly one endpoint vertex." ;
    sh:targetClass :Endpoint ;
    sh:property [
        sh:path :endpointOf ;
        sh:class :Edge ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :endpointVertex ;
        sh:class :Vertex ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] .

:DirectedEdgeShape
    a sh:NodeShape ;
    rdfs:label "directed edge shape" ;
    rdfs:comment "Validates directed edge endpoint ordering, source and target consistency, and rank-1/rank-2 endpoint requirements." ;
    sh:targetClass :DirectedEdge ;
    sh:property [
        sh:path :hasEndpoint ;
        sh:class ord:Comparable ;
        sh:minCount 2 ;
        sh:maxCount 2 ;
    ] ;
    sh:property [
        sh:path :endpointOrder ;
        sh:class ord:TotalOrdering ;
        sh:minCount 1 ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :source ;
        sh:class :Vertex ;
        sh:maxCount 1 ;
    ] ;
    sh:property [
        sh:path :target ;
        sh:class :Vertex ;
        sh:maxCount 1 ;
    ] ;
    sh:sparql [
        sh:message "Each directed-edge endpoint must belong to the edge's endpointOrder and have rank 1 or 2." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            PREFIX ord: <https://ontology.inferal.com/modules/ordering/>
            SELECT $this ?endpoint WHERE {
                $this :endpointOrder ?order ;
                      :hasEndpoint ?endpoint .
                FILTER NOT EXISTS {
                    ?endpoint ord:inOrdering ?order ;
                              ord:rank ?rank .
                    FILTER(?rank = 1 || ?rank = 2)
                }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A directed edge must have exactly one rank-1 endpoint and exactly one rank-2 endpoint in its endpointOrder." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            PREFIX ord: <https://ontology.inferal.com/modules/ordering/>
            SELECT $this WHERE {
                $this :endpointOrder ?order .
                OPTIONAL {
                    $this :hasEndpoint ?first .
                    ?first ord:inOrdering ?order ;
                           ord:rank 1 .
                }
                OPTIONAL {
                    $this :hasEndpoint ?second .
                    ?second ord:inOrdering ?order ;
                            ord:rank 2 .
                }
            }
            GROUP BY $this
            HAVING(COUNT(DISTINCT ?first) != 1 || COUNT(DISTINCT ?second) != 1)
        """ ;
    ] ;
    sh:sparql [
        sh:message "The rank-2 endpoint of a directed edge must sort greater than the rank-1 endpoint." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            PREFIX ord: <https://ontology.inferal.com/modules/ordering/>
            SELECT $this ?first ?second WHERE {
                $this :endpointOrder ?order ;
                      :hasEndpoint ?first, ?second .
                ?first ord:inOrdering ?order ;
                       ord:rank 1 .
                ?second ord:inOrdering ?order ;
                        ord:rank 2 .
                FILTER NOT EXISTS { ?second ord:greaterThan ?first . }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A source assertion must match the rank-1 endpoint vertex." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            PREFIX ord: <https://ontology.inferal.com/modules/ordering/>
            SELECT $this ?source WHERE {
                $this :endpointOrder ?order ;
                      :source ?source .
                FILTER NOT EXISTS {
                    $this :hasEndpoint ?endpoint .
                    ?endpoint ord:inOrdering ?order ;
                              ord:rank 1 ;
                              :endpointVertex ?source .
                }
            }
        """ ;
    ] ;
    sh:sparql [
        sh:message "A target assertion must match the rank-2 endpoint vertex." ;
        sh:select """
            PREFIX : <https://ontology.inferal.com/modules/graphs/>
            PREFIX ord: <https://ontology.inferal.com/modules/ordering/>
            SELECT $this ?target WHERE {
                $this :endpointOrder ?order ;
                      :target ?target .
                FILTER NOT EXISTS {
                    $this :hasEndpoint ?endpoint .
                    ?endpoint ord:inOrdering ?order ;
                              ord:rank 2 ;
                              :endpointVertex ?target .
                }
            }
        """ ;
    ] .

:UndirectedEdgeShape
    a sh:NodeShape ;
    rdfs:label "undirected edge shape" ;
    rdfs:comment "Validates that undirected edges do not carry directed endpoint ordering, source, or target assertions." ;
    sh:targetClass :UndirectedEdge ;
    sh:property [
        sh:path :endpointOrder ;
        sh:maxCount 0 ;
    ] ;
    sh:property [
        sh:path :source ;
        sh:maxCount 0 ;
    ] ;
    sh:property [
        sh:path :target ;
        sh:maxCount 0 ;
    ] .
