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

import static org.apache.jena.graph.Node.ANY;
import static org.apache.jena.sparql.serializer.FmtEltLib.count;
import static org.apache.jena.sparql.serializer.FmtEltLib.createTriplesListBlock;
import static org.apache.jena.sparql.serializer.FmtEltLib.rdfFirst;

import java.util.*;

import org.apache.jena.atlas.io.IndentedLineBuffer;
import org.apache.jena.atlas.io.IndentedWriter;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.Triple;
import org.apache.jena.query.Query;
import org.apache.jena.query.QueryVisitor;
import org.apache.jena.query.Syntax;
import org.apache.jena.sparql.core.BasicPattern;
import org.apache.jena.sparql.core.PathBlock;
import org.apache.jena.sparql.core.TriplePath;
import org.apache.jena.sparql.expr.Expr;
import org.apache.jena.sparql.path.PathWriter;
import org.apache.jena.sparql.syntax.*;
import org.apache.jena.sparql.util.FmtUtils;
import org.apache.jena.vocabulary.RDF;

public class FormatterElement extends FormatterBase implements ElementVisitor {
    public static final int     INDENT                   = 2;

    /** Control whether to show triple pattern boundaries - creates extra nesting */
    public static final boolean PATTERN_MARKERS          = false;

// /** Control whether triple patterns always have a final dot - it can be dropped in some cases */
// public static boolean PATTERN_FINAL_DOT = false ;

    /** Control whether (non-triple) patterns have a final dot - it can be dropped */
    public static final boolean GROUP_SEP_DOT            = false;

    /** Control whether the first item of a group is on the same line as the { */
    public static final boolean GROUP_FIRST_ON_SAME_LINE = true;

    /** Control pretty printing */
    public static final boolean PRETTY_PRINT             = true;

    /** Control pretty printing of RDF lists */
    public static final boolean FMT_LISTS                = true;

    /** Control pretty printing of free standing RDF lists */
    // Do *not* set "true" - argument of property paths can be lists and these span
    // PathBlocks and Basic Graph Patterns and this is not handled properly.
    public static final boolean FMT_FREE_STANDING_LISTS  = false;

    /**
     * Control whether disjunction has set of delimiters - as it's a group usually, these
     * aren't needed
     */
    public static final boolean UNION_MARKERS            = false;

    /** Control whether GRAPH indents in a fixed way or based on the layout size */
    public static final boolean GRAPH_FIXED_INDENT       = true;

    /**
     * Control whether NOT EXIST/EXISTS indents in a fixed way or based on the layout size
     */
    public static final boolean ELEMENT1_FIXED_INDENT    = true;

    /** Control triples pretty printing */
    public static final int     TRIPLES_SUBJECT_COLUMN   = 8;

    // Less than this => rest of triple on the same line
    // Could be smart and make it depend on the property length as well. Later.
    public static final int     TRIPLES_SUBJECT_LONG     = 12;

    public static final int     TRIPLES_PROPERTY_COLUMN  = 20;

    public static final int     TRIPLES_COLUMN_GAP       = 2;

    public FormatterElement(IndentedWriter out, SerializationContext context) {
        super(out, context);
    }

    public static void format(IndentedWriter out, SerializationContext cxt, Element el) {
        FormatterElement fmt = new FormatterElement(out, cxt);
        fmt.startVisit();
        el.visit(fmt);
        fmt.finishVisit();
    }

    public static String asString(Element el) {
        SerializationContext cxt = new SerializationContext();
        IndentedLineBuffer b = new IndentedLineBuffer();
        FormatterElement.format(b, cxt, el);
        return b.toString();
    }

    public boolean topMustBeGroup() {
        return false;
    }

    @Override
    public void visit(ElementTriplesBlock el) {
        if ( el.isEmpty() ) {
            out.println("# Empty BGP");
            return;
        }
        formatTriples(el.getPattern());
    }

    @Override
    public void visit(ElementPathBlock el) {
        // Write path block - don't put in a final trailing "."
        if ( el.isEmpty() ) {
            out.println("# Empty BGP");
            return;
        }

        // Split into BGP-path-BGP-...
        // where the BGPs may be empty.
        PathBlock pBlk = el.getPattern();
        BasicPattern bgp = new BasicPattern();
        boolean first = true;      // Has anything been output?
        for ( TriplePath tp : pBlk ) {
            if ( tp.isTriple() ) {
                bgp.add(tp.asTriple());
                continue;
            }

            if ( !bgp.isEmpty() ) {
                if ( !first )
                    out.println(" .");
                flush(bgp);
                first = false;
            }
            if ( !first )
                out.println(" .");
            // Path (no RDF list output).
            printSubject(tp.getSubject());
            out.print(" ");
            PathWriter.write(out, tp.getPath(), context.getPrologue());
            out.print(" ");
            printObject(tp.getObject());
            first = false;
        }
        // Flush any stored triple patterns.
        if ( !bgp.isEmpty() ) {
            if ( !first )
                out.println(" .");
            flush(bgp);
            first = false;
        }
    }

    @Override
    public void visit(ElementDataset el) {
        // Not implemented.
//        if ( el.getDataset() != null ) {
//            DatasetGraph dsNamed = el.getDataset();
//            out.print("DATASET ");
//            out.incIndent(INDENT);
//            Iterator<Node> iter = dsNamed.listGraphNodes();
//            if ( iter.hasNext() ) {
//                boolean first = false;
//                for ( ; iter.hasNext() ; ) {
//                    if ( !first )
//                        out.newline();
//                    out.print("FROM <");
//                    Node n = iter.next();
//                    out.print(slotToString(n));
//                    out.print(">");
//                }
//            }
//            out.decIndent(INDENT);
//            out.newline();
//        }
//        if ( el.getElement() != null )
//            visitAsGroup(el.getElement());
    }

    @Override
    public void visit(ElementFilter el) {
        out.print("FILTER ");
        Expr expr = el.getExpr();
        FmtExprSPARQL v = new FmtExprSPARQL(out, context);

        // This assumes that complex expressions are bracketted
        // (parens) as necessary except for some cases:
        // Plain variable or constant

        boolean addParens = false;
        if ( expr.isVariable() )
            addParens = true;
        if ( expr.isConstant() )
            addParens = true;

        if ( addParens )
            out.print("( ");
        v.format(expr);
        if ( addParens )
            out.print(" )");
    }

    @Override
    public void visit(ElementAssign el) {
        out.print("LET (");
        out.print("?" + el.getVar().getVarName());
        out.print(" := ");
        FmtExprSPARQL v = new FmtExprSPARQL(out, context);
        v.format(el.getExpr());
        out.print(")");
    }

    @Override
    public void visit(ElementBind el) {
        out.print("BIND(");
        FmtExprSPARQL v = new FmtExprSPARQL(out, context);
        v.format(el.getExpr());
        out.print(" AS ");
        out.print("?" + el.getVar().getVarName());
        out.print(")");
    }

    @Override
    public void visit(ElementData el) {
        QuerySerializer.outputDataBlock(out, el.getVars(), el.getRows(), context);
    }

    @Override
    public void visit(ElementUnion el) {
        if ( el.getElements().size() == 0 ) {
        	// If this is a union of zero elements, do nothing.
            return ;
        }

        if ( el.getElements().size() == 0 ) {
            // If this is a union of one elements, put in a {}-group
            visitAsGroup(el.getElements().get(0));
            return;
        }

        if ( UNION_MARKERS ) {
            out.print("{");
            out.newline();
            out.pad();
        }

        out.incIndent(INDENT);

        boolean first = true;
        for ( Element subElement : el.getElements() ) {
            if ( !first ) {
                out.decIndent(INDENT);
                out.newline();
                out.print("UNION");
                out.newline();
                out.incIndent(INDENT);
            }
            visitAsGroup(subElement);
            first = false;
        }

        out.decIndent(INDENT);

        if ( UNION_MARKERS ) {
            out.newline();
            out.print("}");
        }
    }

    @Override
    public void visit(ElementGroup el) {
        out.print("{");
        int initialRowNumber = out.getRow();
        out.incIndent(INDENT);
        if ( !GROUP_FIRST_ON_SAME_LINE )
            out.newline();

        int row1 = out.getRow();
        out.pad();

        boolean first = true;
        Element lastElt = null;

        for ( Element subElement : el.getElements() ) {
            // Some adjacent elements need a DOT:
            // ElementTriplesBlock, ElementPathBlock
            if ( !first ) {
                // Need to move on after the last thing printed.
                // Check for necessary DOT as separator
                if ( GROUP_SEP_DOT || needsDotSeparator(lastElt, subElement) )
                    out.print(" . ");
                out.newline();
            }
            subElement.visit(this);
            first = false;
            lastElt = subElement;
        }
        out.decIndent(INDENT);

        // Where to put the closing "}"
        int row2 = out.getRow();
        if ( row1 != row2 )
            out.newline();

        // Finally, close the group.
        if ( out.getRow() == initialRowNumber )
            out.print(" ");
        out.print("}");
    }

    private static boolean needsDotSeparator(Element el1, Element el2) {
        return needsDotSeparator(el1) && needsDotSeparator(el2);
    }

    private static boolean needsDotSeparator(Element el) {
        return (el instanceof ElementTriplesBlock) || (el instanceof ElementPathBlock);
    }

    @Override
    public void visit(ElementOptional el) {
        out.print("OPTIONAL");
        out.incIndent(INDENT);
        out.newline();
        visitAsGroup(el.getOptionalElement());
        out.decIndent(INDENT);
    }

    @Override
    public void visit(ElementLateral el) {
        out.print("LATERAL");
        out.incIndent(INDENT);
        out.newline();
        visitAsGroup(el.getLateralElement());
        out.decIndent(INDENT);
    }

    @Override
    public void visit(ElementNamedGraph el) {
        visitNodePattern("GRAPH", el.getGraphNameNode(), el.getElement());
    }

    @Override
    public void visit(ElementService el) {
        String x = "SERVICE";
        if ( el.getSilent() )
            x = "SERVICE SILENT";
        visitNodePattern(x, el.getServiceNode(), el.getElement());
    }

    private void visitNodePattern(String label, Node node, Element subElement) {
        int len = label.length();
        out.print(label);
        out.print(" ");
        String nodeStr = (node == null) ? "*" : slotToString(node);
        out.print(nodeStr);
        len += nodeStr.length();
        if ( GRAPH_FIXED_INDENT ) {
            out.incIndent(INDENT);
            out.newline(); // NB and newline
        } else {
            out.print(" ");
            len++;
            out.incIndent(len);
        }
        visitAsGroup(subElement);

        if ( GRAPH_FIXED_INDENT )
            out.decIndent(INDENT);
        else
            out.decIndent(len);
    }

    private void visitElement1(String label, Element1 el) {

        int len = label.length();
        out.print(label);
        len += label.length();
        if ( ELEMENT1_FIXED_INDENT ) {
            out.incIndent(INDENT);
            out.newline(); // NB and newline
        } else {
            out.print(" ");
            len++;
            out.incIndent(len);
        }
        visitAsGroup(el.getElement());
        if ( ELEMENT1_FIXED_INDENT )
            out.decIndent(INDENT);
        else
            out.decIndent(len);
    }

    @Override
    public void visit(ElementExists el) {
        visitElement1("EXISTS", el);
    }

    @Override
    public void visit(ElementNotExists el) {
        visitElement1("NOT EXISTS", el);
    }

    @Override
    public void visit(ElementMinus el) {
        out.print("MINUS");
        out.incIndent(INDENT);
        out.newline();
        visitAsGroup(el.getMinusElement());
        out.decIndent(INDENT);
    }

    @Override
    public void visit(ElementSubQuery el) {
        out.print("{ ");
        out.incIndent(INDENT);
        Query q = el.getQuery();

        // Serialize with respect to the existing context.
        QuerySerializerFactory factory = SerializerRegistry.get().getQuerySerializerFactory(Syntax.syntaxARQ);
        QueryVisitor serializer = factory.create(Syntax.syntaxARQ, context, out);
        q.visit(serializer);

        out.decIndent(INDENT);
        out.print("}");
    }

    public void visitAsGroup(Element el) {
        boolean needBraces = !((el instanceof ElementGroup) || (el instanceof ElementSubQuery));

        if ( needBraces ) {
            out.print("{ ");
            out.incIndent(INDENT);
        }

        el.visit(this);

        if ( needBraces ) {
            out.decIndent(INDENT);
            out.print("}");
        }
    }

    // -------- Formatting a basic graph pattern
    // Triple order is preserved.

    int subjectWidth   = -1;
    int predicateWidth = -1;

    @Override
    protected void formatTriples(BasicPattern triples) {
        if ( !PRETTY_PRINT ) {
            super.formatTriples(triples);
            return;
        }

        if ( triples.isEmpty() )
            return;

        // Lists in this BasicPattern.
        // TriplesListBlock is a record of lists in this BGP.
        // Formatting is off for list if there is an empty TriplesListBlock.
        // This is cautionsly spotting the triples from RDF lists as generated by the
        // parser.

        TriplesListBlock block = FMT_LISTS
            ? createTriplesListBlock(triples)
            : new TriplesListBlock();

        Set<Node> freeStanding = new HashSet<>();
        for ( Node head : block.listElementsMap.keySet() ) {
            // Check for suitablity to print.
            // See also FmtEltLib#collectList
            //
            // Subject-list : inCount = 0, outCount = 3
            // Object-list :  inCount = 1, outCount = 2
            // Free-standing list :  inCount = 0, outCount = 2
            //     Free-standing list is handled as a special case.
            int inCount = count(triples.getList(), ANY, ANY, head);
            int outCount = count(triples.getList(), head, ANY, ANY);
            if ( inCount == 0 && outCount == 2 )
                // Free standing.
                freeStanding.add(head);
        }

        setWidths(triples);
        if ( subjectWidth > TRIPLES_SUBJECT_COLUMN )
            subjectWidth = TRIPLES_SUBJECT_COLUMN;
        if ( predicateWidth > TRIPLES_PROPERTY_COLUMN )
            predicateWidth = TRIPLES_PROPERTY_COLUMN;

        // Accumulate all triples with the same subject.
        List<Triple> subjAcc = new ArrayList<>();
        // Subject being accumulated
        Node subj = null;
        // Print newlines between blocks.
        boolean first = true;

        int indent = -1;
        for ( Triple t : triples ) {
            if ( block.triplesInLists.contains(t) ) {
                if ( rdfFirst.equals(t.getPredicate()) ) {
                    if ( freeStanding.contains(t.getSubject()) )
                        printNodeOrList(t.getSubject(), block.listElementsMap);
                }
                continue;
            }

            if ( subj != null && t.getSubject().equals(subj) ) {
                subjAcc.add(t);
                continue;
            }

            if ( subj != null ) {
                if ( !first )
                    out.println(" .");
                formatSameSubject(subj, subjAcc, block.listElementsMap);
                first = false;
                // At end of line of a block of triples with same subject.
                // Drop through and start new block of same subject triples.
            }

            // New subject
            subj = t.getSubject();
            subjAcc.clear();
            subjAcc.add(t);
        }

        // Flush accumulator
        if ( subj != null && subjAcc.size() != 0 ) {
            if ( !first )
                out.println(" .");
            first = false;
            formatSameSubject(subj, subjAcc, block.listElementsMap);
        }
    }

    // ----

    private void flush(BasicPattern bgp) {
        formatTriples(bgp);
        bgp.getList().clear();
    }

    private void formatSameSubject(Node subject, List<Triple> triples, Map<Node, List<Node>> lists) {

        if ( triples == null || triples.size() == 0 )
            return;

        // Do the first triple.
        Iterator<Triple> iter = triples.iterator();
        Triple t1 = iter.next();

        int indent = subjectWidth + TRIPLES_COLUMN_GAP;

        int s1_len = printNodeOrList(subject, lists);
        if ( s1_len > TRIPLES_SUBJECT_LONG ) {
            out.incIndent(indent);
            out.println();
        } else {
            printGap();
            out.incIndent(indent);
        }

        // Remainder of first triple
        printProperty(t1.getPredicate());
        printGap();

        printNodeOrList(t1.getObject(), lists);

        // Do the rest
        for ( ; iter.hasNext() ; ) {
            Triple t = iter.next();
            out.println(" ;");
            printProperty(t.getPredicate());
            printGap();
            printNodeOrList(t.getObject(), lists);
            continue;
        }

        // Finish off the block.
        out.decIndent(indent);
        // out.print(" .") ;
    }

    private int printNodeOrList(Node node, Map<Node, List<Node>> lists) {
        if ( lists.containsKey(node) )
            return printList(lists.get(node), lists);
        else
            return printNoCol(node);
    }

    private void setWidths(BasicPattern triples) {
        subjectWidth = -1;
        predicateWidth = -1;

        for ( Triple t : triples ) {
            String s = slotToString(t.getSubject());
            if ( s.length() > subjectWidth )
                subjectWidth = s.length();

            String p = slotToStringProperty(t.getPredicate());
            if ( p.length() > predicateWidth )
                predicateWidth = p.length();
        }
    }

    private void printGap() {
        out.print(' ', TRIPLES_COLUMN_GAP);
    }

    // printSubject, printObject - used in ElementPathBlock.
    private int printSubject(Node s) {
        return printNoCol(s);
    }

    // Unadorned "<http://www.w3.org/1999/02/22-rdf-syntax-ns#type>"
    private static String RDFTYPE = FmtUtils.stringForNode(RDF.Nodes.type, new SerializationContext());

    private int printProperty(Node p) {
        String str = slotToStringProperty(p);
        out.print(str);
        out.pad(predicateWidth);
        return out.getCol();
    }

    // String for property - includes the rule for whether to use "a" or not.
    private String slotToStringProperty(Node p) {
        String str = slotToString(p);
        // If rdf:type but no rdf: then str is full URI and we use the "a" form
        if ( str.equals(RDFTYPE) )
            return "a";
        return str;
    }

    private int printObject(Node obj) {
        return printNoCol(obj);
    }

    private int printList(List<Node> list, Map<Node, List<Node>> lists) {
        if ( list.isEmpty() ) {
            out.print("()");
            return 2;
        }

        int col0 = out.getCol();
        out.print("( ");
        for ( Node n : list ) {
            printNodeOrList(n, lists);
            out.print(" ");
        }
        out.print(")");
        int col1 = out.getCol();
        return col1 - col0;
    }

    private int printNoCol(Node node) {
        String str = slotToString(node);
        out.print(str);
        return str.length();
    }
}
