/*
 * 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
 *
 *   https://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.
 *
 *   SPDX-License-Identifier: Apache-2.0
 */

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 explicit 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;

    /** @deprecated Prefer {@link Quad#create(Node, Triple)}. */
    @Deprecated
    public Quad(Node graph, Triple triple) {
        this(graph, triple.getSubject(), triple.getPredicate(), triple.getObject());
    }

    /** @deprecated Prefer {@link Quad#create(Node, Node, Node, Node)}. */
    @Deprecated
    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;
    }

    /**
     * Create a {@code Quad}. Nulls are not allowed for subject/predicate/object.
     * <p>
     * Null for graph means this is a triple, but for type checking reasons, it is
     * being carried as a {@code Quad}.
     */
    public static Quad create(Node g, Node s, Node p, Node o) {
        return new Quad(g, s, p, o);
    }

    /**
     * Create a {@code Quad}. Nulls for for subject/predicate/object are converted to
     * {@link Node#ANY}.
     * <p>
     * Null for graph means this is a triple, but for type checking
     * reasons, it is being carried as a {@code Quad}.
     */
    public static Quad createMatch(Node g, Node s, Node p, Node o) {
        return Quad.create(g, nullToAny(s), nullToAny(p), nullToAny(o));
    }

    public static Quad create(Node g, Triple t) {
        Objects.requireNonNull(t);
        return new Quad(g, t);
    }

    private static Node anyToNull( Node n )
    { return Node.ANY.equals( n ) ? null : n; }

    private static Node nullToAny( Node n )
    { return n == null ? Node.ANY : n; }

    private static boolean isAny(Node n)
    { return n == null || Node.ANY.equals(n); }

    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 the subject/predicate/object as a triple.
     */
    public Triple asTriple() {
        return Triple.create(subject, predicate, object);
    }

    public boolean isConcrete() {
        return subject.isConcrete() && predicate.isConcrete() && object.isConcrete() && (graph == null || graph.isConcrete());
    }

    /**
     * Does this quad, match the other quad, allowing for wildcards.
     * The wildcard node is {@link Node#ANY} and it matches any node, including a wildcard.
     * Both this quad and the argument quad may contain wildcards,
     * that is "matches" is symmetric:
     * {@code this.matches(that) == that.matches(this)}.
     */
    public boolean matches(Quad other) {
        return matches(other.getGraph(), other.getSubject(), other.getPredicate(), other.getObject());
    }

    public boolean matches(Node g, Node s, Node p, Node o) {
        return matches(this.graph, g) && matches(this.subject, s) && matches(this.predicate, p) && matches(this.object, o);
    }

    /** Match with possible wildcards (Node.ANY) in either argument. */
    private static boolean matches(Node patternNode, Node node) {
        if ( isAny(patternNode) )
            return true;
        if ( isAny(node) )
            return true;
        return patternNode.sameTermAs(node);
    }

    /**
     * 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());
    }

    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 quad) )
            return false;

        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;
    }

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