/**
 * 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.commons.rdf.jena.impl;

import java.util.Objects;
import java.util.Optional;
import java.util.UUID;

import org.apache.commons.rdf.api.QuadLike;
import org.apache.commons.rdf.api.RDFTerm;
import org.apache.commons.rdf.jena.JenaQuad;
import org.apache.commons.rdf.jena.JenaQuadLike;
import org.apache.commons.rdf.jena.JenaRDF;
import org.apache.commons.rdf.jena.JenaRDFTerm;
import org.apache.commons.rdf.jena.JenaTriple;
import org.apache.jena.graph.Triple;
import org.apache.jena.sparql.core.Quad;

/**
 * A generalized {@link QuadLike}, backed by a Jena {@link Quad} or
 * {@link Triple}.
 * <p>
 * This class does not implement any particular {@link #equals(Object)} or
 * {@link #hashCode()} but can otherwise be used as a base class for both a
 * {@link JenaTriple} and a {@link JenaQuad}.
 *
 * @see JenaTripleImpl
 * @see JenaQuadImpl
 * @see internalJenaFactory#createGeneralizedTriple(RDFTerm, RDFTerm, RDFTerm)
 * @see internalJenaFactory#createGeneralizedQuad(RDFTerm, RDFTerm, RDFTerm,
 *      RDFTerm)
 *
 */
abstract class AbstractQuadLike<S extends RDFTerm, P extends RDFTerm, O extends RDFTerm, G extends RDFTerm>
        implements JenaQuadLike<G> {

    private static InternalJenaFactory internalJenaFactory = new InternalJenaFactory() {
    };

    /**
     * COMMONSRDF-55 - special handling of urn:x-arq:DefaultGraph and friends
     * <p>
     * This can recognize <urn:x-arq:DefaultGraph> and
     * <urn:x-arq:DefaultGraphNode> from any IRI instance, so they can be
     * replaced with Optional.empty(). Note that this code does not hardcode the
     * internal Jena IRIs but uses Jena's constants {@link Quad#defaultGraphIRI}
     * and {@link Quad#defaultGraphNodeGenerated}.
     */
    private static class DefaultGraphChecker {
        // Fixed UUID for comparison of defaultGraphNodeGenerated
        private final UUID salt = UUID.fromString("aaa6bf96-ea58-4a55-9485-3733403a1f24");
        private final RDFTerm defaultGraph = internalJenaFactory.createRDFTerm(Quad.defaultGraphIRI, salt);
        private final RDFTerm defaultGraphNodeGenerated = internalJenaFactory.createRDFTerm(Quad.defaultGraphNodeGenerated, salt);

        /**
         * Check if RDFTerm is an IRI that matches the two Jena default graph
         * constants (Even if they are from another RDF implementation).
         * <p>
         * This checker is "softer" than {@link #isNotDefaultGraphJenaNode(RDFTerm)}
         *
         * @param graphName
         *            potential graph name IRI or BlankNode
         * @return <code>true</code> if the RDFTerm does not indicate a default
         *         graph in Jena
         */
        public boolean isNotDefaultGraph(final RDFTerm graphName) {
            return !(graphName.equals(defaultGraph) || graphName.equals(defaultGraphNodeGenerated));
        }

        /**
         * Check if RDFTerm has an IRI that matches the two Jena default graph
         * constants (but only if it is an JenaRDFTerm instance)
         *
         * @param graphName
         *            potential graph name IRI or BlankNode
         * @return <code>true</code> if the RDFTerm does not indicate a default
         *         graph in Jena
         */
        public boolean isNotDefaultGraphJenaNode(final RDFTerm graphName) {
            return ! (graphName instanceof JenaRDFTerm) ||
                    ! Quad.isDefaultGraph(((JenaRDFTerm)graphName).asJenaNode());

        }
    }

    private static DefaultGraphChecker defaultGraphChecker = new DefaultGraphChecker();

    final Optional<G> graphName;
    final S subject;
    final P predicate;
    final O object;
    org.apache.jena.sparql.core.Quad quad = null;
    org.apache.jena.graph.Triple triple = null;


    AbstractQuadLike(final S subject, final P predicate, final O object, final Optional<G> graphName) {
        this.subject = Objects.requireNonNull(subject);
        this.predicate = Objects.requireNonNull(predicate);
        this.object = Objects.requireNonNull(object);
        // Enforce
        this.graphName = Objects.requireNonNull(graphName).filter(defaultGraphChecker::isNotDefaultGraphJenaNode);
    }

    AbstractQuadLike(final S subject, final P predicate, final O object) {
        this(subject, predicate, object, Optional.empty());
    }

    @SuppressWarnings("unchecked")
    AbstractQuadLike(final org.apache.jena.sparql.core.Quad quad, final UUID salt) {
        this.quad = Objects.requireNonNull(quad);
        this.subject = (S) internalJenaFactory.createRDFTerm(quad.getSubject(), salt);
        this.predicate = (P) internalJenaFactory.createRDFTerm(quad.getPredicate(), salt);
        this.object = (O) internalJenaFactory.createRDFTerm(quad.getObject(), salt);
        if (quad.isDefaultGraph()) {
            this.graphName = Optional.empty();
        } else {
            this.graphName = Optional.of((G) internalJenaFactory.createRDFTerm(quad.getGraph(), salt));
        }
    }

    @SuppressWarnings("unchecked")
    AbstractQuadLike(final org.apache.jena.graph.Triple triple, final UUID salt) {
        this.triple = Objects.requireNonNull(triple);
        this.subject = (S) internalJenaFactory.createRDFTerm(triple.getSubject(), salt);
        this.predicate = (P) internalJenaFactory.createRDFTerm(triple.getPredicate(), salt);
        this.object = (O) internalJenaFactory.createRDFTerm(triple.getObject(), salt);
        this.graphName = Optional.empty();
    }

    @Override
    public org.apache.jena.sparql.core.Quad asJenaQuad() {
        final JenaRDF factory = new JenaRDF();
        if (quad == null) {
            quad = org.apache.jena.sparql.core.Quad.create(
                    graphName.map(factory::asJenaNode).orElse(Quad.defaultGraphIRI),
                    factory.asJenaNode(subject),
                    factory.asJenaNode(predicate),
                    factory.asJenaNode(object));
        }
        return quad;
    }

    @Override
    public org.apache.jena.graph.Triple asJenaTriple() {
        final JenaRDF factory = new JenaRDF();
        if (triple == null) {
            triple = org.apache.jena.graph.Triple.create(
                    factory.asJenaNode(subject),
                    factory.asJenaNode(predicate),
                    factory.asJenaNode(object));
        }
        return triple;
    }

    @Override
    public S getSubject() {
        return subject;
    }

    @Override
    public P getPredicate() {
        return predicate;
    }

    @Override
    public O getObject() {
        return object;
    }

    @Override
    public Optional<G> getGraphName() {
        return graphName;
    }

    @Override
    public String toString() {
        // kind of nquad syntax
        return getSubject().ntriplesString() + " " + getPredicate().ntriplesString() + " "
                + getObject().ntriplesString() + " " + getGraphName().map(RDFTerm::ntriplesString).orElse("") + ".";
    }

}
