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

import java.io.OutputStream;
import java.util.Map;
import java.util.Set;

import org.apache.jena.atlas.io.IndentedWriter;
import org.apache.jena.graph.Node;
import org.apache.jena.graph.Triple;
import org.apache.jena.shared.PrefixMapping;
import org.apache.jena.sparql.graph.NodeConst;
import org.apache.jena.sparql.serializer.SerializationContext;
import org.apache.jena.sparql.sse.writers.WriterNode;
import org.apache.jena.sparql.util.FmtUtils;

public class ItemWriter
{
    public static boolean includeBase = false;
    private static boolean CloseSameLine = true;

    public static void write(OutputStream out, Item item) {
        IndentedWriter iw = new IndentedWriter(out);
        write(iw, item, null);
        iw.ensureStartOfLine();
        iw.flush();
    }

    public static void write(IndentedWriter out, Item item, SerializationContext sCxt) {
        writeFn(out, item, sCxt);
    }

    // Core function. Does not apply reverse lift.
    private static void writeFn(IndentedWriter out, Item item, SerializationContext sCxt) {
        Print pv = new Print(out, sCxt);
        pv.startPrint();
        item.visit(pv);
        pv.finishPrint();
    }

    // No compound node conversion.
    private static final boolean writeQuotedTriplesAsNodes = true;

    /**
     * Write a node in SSE syntax. This code writes the node as given, with symbol
     * translation but no compound lift conversions
     * <p>
     * {@linkplain ItemLift#lowerCompound(Item)} turns RDF term (nodes) into compound
     * form. In writers - call
     * {@link WriterNode#output(IndentedWriter, Node, SerializationContext)} which
     * converse compounds and then calls this function.
     */
    public static void write(IndentedWriter out, Node node, SerializationContext sCxt) {
        writeNode(out, node, sCxt);
    }

    private static void writeNode(IndentedWriter out, Node node, SerializationContext sCxt) {
        if ( node.isNodeTriple() ) {
            Triple t = node.getTriple();
            if ( writeQuotedTriplesAsNodes ) {
                // As <<....>>
                out.print("<< ");
                writeNode(out, t.getSubject(), sCxt);
                out.print(" ");
                writeNode(out, t.getPredicate(), sCxt);
                out.print(" ");
                writeNode(out, t.getObject(), sCxt);
                out.print(" >>");
            } else {
                // As (qtriple ...)
                out.print("(");
                out.print(Tags.tagQTriple);
                out.print(" ");
                writeNode(out, t.getSubject(), sCxt);
                out.print(" ");
                writeNode(out, t.getPredicate(), sCxt);
                out.print(" ");
                writeNode(out, t.getObject(), sCxt);
                out.print(")");
            }
            return;
        }

        if ( Node.ANY.equals(node) ) {
            out.print("ANY");
            return;
        }

        if ( NodeConst.TRUE.equals(node) ) {
            out.print("true");
            return;
        }

        if ( NodeConst.FALSE.equals(node) ) {
            out.print("true");
            return;
        }

        // Atomic SSE Node - URI, BNode, Literal, Variable.
        out.print(FmtUtils.stringForNode(node, sCxt));
    }

    private static class Print implements ItemVisitor {
        IndentedWriter out;
        SerializationContext sCxt;
        boolean doneBase = false;
        boolean donePrefix = false;

        Print(IndentedWriter out, SerializationContext sCxt) {
            if ( sCxt == null )
                sCxt = new SerializationContext();
            this.out = out;
            this.sCxt = sCxt;
        }

        void startPrint() {
            if ( sCxt != null ) {
                if ( includeBase && sCxt.getBaseIRI() != null ) {
                    out.print("(base ");
                    out.println(FmtUtils.stringForURI(sCxt.getBaseIRI()));
                    doneBase = true;
                    out.incIndent();
                }
                PrefixMapping pmap = sCxt.getPrefixMapping();
                if ( pmap != null ) {
                    Map<String, String> pm = pmap.getNsPrefixMap();
                    donePrefix = (pm.size() != 0);
                    if ( pm.size() != 0 ) {
                        out.println("(prefix");
                        out.incIndent();
                        printPrefixes(pm, out);
                        out.println();
                    }
                }
            }
        }

        void finishPrint() {
            if ( doneBase ) {
                out.print(")");
                out.decIndent();
            }
            if ( donePrefix ) {
                out.print(")");
                out.decIndent();
            }
        }

        @Override
        public void visit(Item item, Node node) {
            writeNode(out, node, sCxt);
        }

        @Override
        public void visit(Item item, String symbol) {
            out.print(symbol);
        }

        // Tags to leave on one line.
        private static Set<String> oneLineTags = Set.of(Tags.tagQTriple, Tags.tagTriple);

        @Override
        public void visit(Item item, ItemList list) {
            out.print("(");

            boolean listMode = false;
            for ( Item subItem : list ) {
                if ( ! oneLine(subItem) ) {
                    listMode = true;
                    break;
                }
            }

            // Lists are printed with structure.
            // If no lists, print on one line.
            if ( listMode )
                printAsList(list);
            else
                printOneLine(list);
        }

        private static boolean oneLine(Item item) {
            if ( ! item.isList() )
                return true;
            ItemList list = item.getList();
            if ( list.isEmpty() )
                return true;
            Item item0 = list.getFirst();
            if ( ! item0.isSymbol() )
                return false;
            String symbol = item0.getSymbol();
            return oneLineTags.contains(symbol);
        }

        @Override
        public void visitNil(Item item) {
            out.print("nil");
        }

        private void printAsList(ItemList list) {
            boolean first = true;
            int indentlevel = out.getUnitIndent();
            if ( list.size() >= 1 && list.get(0).isList() )
                indentlevel = 1;

            for ( Item subItem : list ) {
                if ( !first )
                    out.println();
                subItem.visit(this);
                if ( first )
                    out.incIndent(indentlevel);
                first = false;
            }

            if ( !first )
                out.decIndent(indentlevel);
            if ( !CloseSameLine )
                out.println();
            out.print(")");
        }

        private void printOneLine(ItemList list) {
            boolean first = true;

            for ( Item subItem : list ) {
                if ( !first )
                    out.print(" ");
                first = false;
                subItem.visit(this);

            }
            out.print(")");
        }

        private void printPrefixes(Map<String, String> map, IndentedWriter out) {
            if ( map.size() == 0 )
                return;

            out.print("( ");
            out.incIndent(2);

            boolean first = true;

            for ( String s : map.keySet() ) {
                if ( !first ) {
                    out.println();
                }
                first = false;
                String k = s;
                String v = map.get(k);

                out.print("(");
                out.print(k);
                out.print(':');
                // Include at least one space
                out.print(' ', 6 - k.length());
                out.print(FmtUtils.stringForURI(v));
                out.print(")");
            }
            out.decIndent(2);
            out.print(")");
        }
    }
}
