/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.jena.sparql.core;

import java.io.IOException;
import java.io.ObjectStreamException;
import java.io.Serializable;
import java.util.Objects;
import java.util.function.Function;

import org.apache.jena.graph.Node ;
import org.apache.jena.graph.NodeFactory ;
import org.apache.jena.graph.Triple ;
import org.apache.jena.riot.system.Serializer;

public class Quad implements Serializable 
{    
    // Create QuadNames? GraphNames?
    
    /** Name of the default for explict use in GRAPH */
    public static final Node defaultGraphIRI        =  NodeFactory.createURI("urn:x-arq:DefaultGraph") ;

    /** Name of the default graph as used by parsers and in quad form of algebra. 
     *  Not for access to the default graph by name - use Quad.defaultGraphIRI.
     */ 
    public static final Node defaultGraphNodeGenerated     =  NodeFactory.createURI("urn:x-arq:DefaultGraphNode") ;
    
    /** Name of the merge of all named graphs (use this for the graph of all named graphs) */
    public static final Node unionGraph           =  NodeFactory.createURI("urn:x-arq:UnionGraph") ;

    /** Name of the non-graph when a quad is really a triple - also parsing of triples formats 
     *  (and the default graph when parsing N-Quads or TriG) 
     *  Not for access to the default graph by name - use Quad.defaultGraphIRI.
     */
    public static final Node tripleInQuad           =  null ;
    
    /** A {@code Quad} that has a wildcard in all fields. */
    public static final Quad ANY = Quad.create( Node.ANY, Node.ANY, Node.ANY, Node.ANY );
    
    private final Node graph, subject, predicate, object ;
    
    public Quad(Node graph, Triple triple)
    {
        this(graph, triple.getSubject(), triple.getPredicate(), triple.getObject()) ;
    }
    
    public Quad(Node g, Node s, Node p, Node o)
    {
        // Null means it's a triple really.
//        if ( g == null )
//            throw new UnsupportedOperationException("Quad: graph cannot be null");
        if ( s == null )
            throw new UnsupportedOperationException("Quad: subject cannot be null");
        if ( p == null )
            throw new UnsupportedOperationException("Quad: predicate cannot be null");
        if ( o == null )
            throw new UnsupportedOperationException("Quad: object cannot be null");
        this.graph = g ;
        this.subject = s ;
        this.predicate = p ;
        this.object = o ;
    }
    
    public static Quad create(Node g, Node s, Node p, Node o)   { return new Quad(g,s,p,o) ; }
    public static Quad create(Node g, Triple t)                 { return new Quad(g,t) ; }

    public final Node getGraph()      { return graph ; }
    public final Node getSubject()    { return subject ; }
    public final Node getPredicate()  { return predicate ; }
    public final Node getObject()     { return object ; }

    /** Get as a triple - useful because quads often come in blocks for the same graph */  
    public Triple asTriple()
    { 
        return new Triple(subject, predicate, object) ;
    }
    
    public boolean isConcrete()
    {
        return subject.isConcrete() && predicate.isConcrete() && object.isConcrete() && graph.isConcrete() ;
    }
    
    /** Test whether this is a quad for the default graph (not the default graphs by explicit name) */
    public static boolean isDefaultGraphGenerated(Node node)
    {
        // The node used by the quad generator for the default graph 
        // Not the named graph that refers to the default graph.
        return defaultGraphNodeGenerated.equals(node) ;
    }
    
    /** Default, explicitly named concrete graph */
    public static boolean isDefaultGraphExplicit(Node node)
    {
        return defaultGraphIRI.equals(node) ; 
    }
    
    /** Default, concrete graph (either generated or explicitly named) -- not triple-in-quad*/
    public static boolean isDefaultGraph(Node node)
    {
        return isDefaultGraphGenerated(node) || isDefaultGraphExplicit(node) ; 
    }

    /** Default, concrete graph (either generated or explicitly named) -- not triple-in-quad*/
    public static boolean isUnionGraph(Node node)
    {
        return unionGraph.equals(node) ; 
    }


    /** Default, concrete graph via generated URI (not explicitly, named) */
    public boolean isDefaultGraphExplicit()
    { return isDefaultGraphExplicit(getGraph()) ; }
    
    /** Default graph, explicitly named (not generated) */
    public boolean isDefaultGraphGenerated()
    {
        return  isDefaultGraphGenerated(getGraph()) ;
    }

    /** Default, concrete graph (either generated or explicitly named) */
    public boolean isDefaultGraph()
    {
        return  isDefaultGraph(getGraph()) ;
    }
    
    /** node used for the RDF merge of named graphs */
//    public static boolean isQuadUnionGraph(Node node)
//    {
//        return node.equals(unionGraph) ;
//    }
//    
    public boolean isUnionGraph()           { return isUnionGraph(graph) ; }

    /** Is it really a triple? */  
    public boolean isTriple()               { return Objects.equals(graph, tripleInQuad) ; } 

    /** Is this quad a legal data quad (legal data triple, IRI for graph) */   
    public boolean isLegalAsData()
    {
        Node sNode = getSubject() ;
        Node pNode = getPredicate() ;
        Node oNode = getObject() ;
        Node gNode = getGraph() ;
        
        if ( sNode.isLiteral() || sNode.isVariable() )
            return false ;
        
        if ( ! pNode.isURI() )  // Not variable, literal or blank.
            return false ;

        if ( oNode.isVariable() )
            return false ;
        
        if ( gNode != null ) {
            if ( ! gNode.isURI() && ! gNode.isBlank() )
                return false ;
        }
        
        return true ;
    }
    
    // ---- Serializable
    protected Object writeReplace() throws ObjectStreamException {
        Function<Quad, Object> function =  Serializer.getQuadSerializer() ;
        if ( function == null )
            throw new IllegalStateException("Function for Quad.writeReplace not set") ;
        return function.apply(this);
    }
    // Any attempt to serialize without replacement is an error.
    private void writeObject(java.io.ObjectOutputStream out) throws IOException {
        throw new IllegalStateException(); 
    }
    private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new IllegalStateException();
    }
    // ---- Serializable
    
    @Override
    public int hashCode() 
    { 
        int x = 
               (subject.hashCode() >> 1) ^ 
               predicate.hashCode() ^ 
               (object.hashCode() << 1);
        if ( graph != null )
            x ^= (graph.hashCode()>>2) ;
        else
            x++ ;
        return x ;
    }
    
    
    
    @Override
    public boolean equals(Object other) 
    { 
        if ( this == other ) return true ;

        if ( ! ( other instanceof Quad) )
            return false ;
        Quad quad = (Quad)other ;
        
        if ( ! Objects.equals(graph, quad.graph) ) return false ;
        if ( ! subject.equals(quad.subject) ) return false ;
        if ( ! predicate.equals(quad.predicate) ) return false ;
        if ( ! object.equals(quad.object) ) return false ;
        return true ;
    }
    
    public boolean matches(Node g, Node s, Node p, Node o)
    {
        return nodeMatches(getGraph(), g) && nodeMatches(getSubject(), s) &&
               nodeMatches(getPredicate(), p) && nodeMatches(getObject(), o) ;
    }

    private static boolean nodeMatches(Node thisNode, Node otherNode)
    {
        // otheNode may be Node.ANY, and this works out.
        return otherNode.matches(thisNode) ;
    }

    @Override
    public String toString()
    {
        String str = (graph==null)?"_":graph.toString() ;
        return "["+str+" "+subject.toString()+" "+predicate.toString()+" "+object.toString()+"]" ;
    }
}
