/**
 * 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.riot.writer;

import java.io.OutputStream ;
import java.io.Writer ;
import java.util.Collection ;
import java.util.List ;
import java.util.Objects;

import org.apache.jena.atlas.io.IndentedWriter ;
import org.apache.jena.graph.Node ;
import org.apache.jena.graph.Triple ;
import org.apache.jena.sparql.core.Quad ;
import org.apache.jena.sparql.util.Context;
import org.apache.jena.system.G;

/** An output of triples / quads that print batches of same subject / same graph, same subject.
 *  It writes something that is easier to read than
 *  N-triples, N-quads but it's not full pretty printing
 *  which usually requires analysing the data before any output.
 *
 *  If fed quads and triples, the output is valid TriG.
 *  If fed only triples, the output is valid Turtle.
 */

public class WriterStreamRDFBlocks extends WriterStreamRDFBatched
{
    protected static final boolean NL_GDFT_START    =  WriterConst.NL_GDFT_START ;
    protected static final boolean NL_GNMD_START    =  WriterConst.NL_GNMD_START ;
    protected static final boolean NL_GDFT_END      =  WriterConst.NL_GDFT_END ;
    protected static final boolean NL_GNMD_END      =  WriterConst.NL_GNMD_END ;

    // Subject column - normal width
    protected static final int INDENT_PREDICATE     = WriterConst.INDENT_PREDICATE ;
    protected static final int MIN_PREDICATE        = 6 ; //WriterConst.MIN_PREDICATE ;
    protected static final int LONG_PREDICATE       = WriterConst.LONG_PREDICATE ;
    protected static final int LONG_SUBJECT         = WriterConst.LONG_SUBJECT ;

    protected static final int INDENT_MIN_S         = 6 ;         // Range of subject indent
    protected static final int INDENT_MAX_S         = 14 ;
    protected static final int GAP_S_P              = 2 ;
    protected static final int GAP_P_O              = 2 ;

    protected static final int INDENT_GDFT          = 2 ;           // Default graph indent
    protected static final int INDENT_GNMD          = 4 ;           // Named graph indent

    // Quad output
    protected Node lastGraph            = null ;
    protected Node lastSubject          = null ;
    protected int currentGraphIndent    = 0;
    // Set true if we see a prefix setting for the RDF namespace.
    protected boolean hasRDFprefix      = false;

    public WriterStreamRDFBlocks(OutputStream output, Context context) {
        super(output, context) ;
    }

    public WriterStreamRDFBlocks(Writer output, Context context) {
        super(output, context) ;
    }

    public WriterStreamRDFBlocks(IndentedWriter output, Context context) {
        super(output, context) ;
    }

    @Override
    protected void printBatchQuads(Node g, Node s, List<Quad> quads) {
        if ( g == null )
            // print as Triples has currentGraph == null.
            g = Quad.defaultGraphNodeGenerated ;
        if ( Objects.equals(g, lastGraph) ) {
            // Same graph, different subject.
            out.println(" .") ;
        } else {
            // Different graph
            endGraph(g) ;
            // This does the startBatch.
            startGraph(g) ;
            lastGraph = g ;
        }
        List<Triple> triples = G.quads2triples(quads.iterator()).toList() ;
        printBatch(s, triples) ;
        // No trailing "." has been printed.
        lastSubject = s ;
    }

    @Override
    protected void printBatchTriples(Node s, List<Triple> triples) {
        startBatch();
        printBatch(s, triples) ;
        out.println(" .") ;
        lastGraph = null;
    }

    private void startBatch() {
        if ( lastGraph != null ) {
            // last batch was quads
            endGraph(null) ;
            lastGraph = null;
        } else {
            // last batch was triples.
        }

        // Any output so far? prefixes or a previous graph.
        if ( out.getRow() > 1 )
            out.println() ;
        out.flush();
    }

    private void printBatch(Node s, List<Triple> triples) {
        outputNode(s) ;
        if ( out.getCol() > LONG_SUBJECT )
            out.println() ;
        else
            gap(GAP_S_P) ;
        out.incIndent(INDENT_PREDICATE) ;
        out.pad() ;
        writePredicateObjectList(triples) ;
        out.decIndent(INDENT_PREDICATE) ;
    }

    private void writePredicateObjectList(Collection<Triple> triples) {
        // Find width
        // We may have a prefix for RDF otherwise we use the 'a' abbreviation for rdf:type.
        boolean writeKeyWordType = countPrefixesForRDF <= 0 ;
        int predicateMaxWidth = Widths.calcWidthTriples(pMap, baseURI, triples, MIN_PREDICATE, LONG_PREDICATE, writeKeyWordType) ;
        boolean first = true ;
        for ( Triple triple : triples ) {
            if ( !first )
                out.println(" ;") ;
            else
                first = false ;

            Node p = triple.getPredicate() ;
            printProperty(p);
            out.pad(predicateMaxWidth) ;
            out.print(' ', GAP_P_O) ;
            Node o = triple.getObject() ;
            outputNode(o) ;
        }
    }

    private void gap(int gap) {
        out.print(' ', gap) ;
    }

    @Override
    protected void finalizeRun() {
        if ( lastGraph != null )
            // last was a quad
            endGraph(null) ;
    }

    @Override
    protected void prefixSetup(String prefix, String iri) {
        // Newline between blocks.
        // After flush, before writing PREFIX
        boolean addNL = ( activeTripleData || activeQuadData );
        if ( addNL )
            out.println();
    }

    protected boolean dftGraph()        { return lastGraph == Quad.defaultGraphNodeGenerated ; }
    protected boolean dftGraph(Node g)  { return g == Quad.defaultGraphNodeGenerated ; }

    protected void startGraph(Node g) {
        startBatch();
        // Start graph
        if ( lastGraph == null ) {
            boolean NL_START = (dftGraph(g) ? NL_GDFT_START : NL_GNMD_START) ;

            lastSubject = null ;
            if ( !dftGraph(g) ) {
                outputNode(g) ;
                out.print(" ") ;
            }

            if ( NL_START )
                out.println("{") ;
            else
                out.print("{ ") ;

            if ( dftGraph() )
                setGraphIndent(INDENT_GDFT) ;
            else {
                int x = NL_START ? INDENT_GNMD : out.getCol() ;
                setGraphIndent(x) ;
            }
            out.incIndent(graphIndent()) ;
        }
        lastGraph = g ;
    }

    protected void endGraph(Node g) {
        if ( lastGraph == null )
            return ;

        // End of graph
        if ( !Objects.equals(lastGraph, g) ) {
            boolean NL_END = (dftGraph(g) ? NL_GDFT_END : NL_GNMD_END) ;

            if ( lastSubject != null )
                out.print(" .") ;
            if ( NL_END ) {
                // } on a new line.
                out.decIndent(graphIndent()) ;
                out.println() ;
                out.println("}") ;
            } else {
                // Possibly on same line as last quad/triple.
                out.decIndent(graphIndent()) ;
                if ( out.atLineStart() )
                    out.println("}") ;
                else
                    out.println(" }") ;
            }
            lastSubject = null ;
            lastGraph = null ;
        }
    }

    protected void setGraphIndent(int x)    { currentGraphIndent = x ; }
    protected int graphIndent()             { return currentGraphIndent ; }
}
