/*

   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.batik.transcoder.svg2svg;

import java.io.IOException;
import java.io.Writer;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

import org.apache.batik.xml.XMLUtilities;

/**
 * This class is responsible of the output of XML constructs.
 *
 * @author <a href="mailto:stephane@hillion.org">Stephane Hillion</a>
 * @version $Id$
 */
public class OutputManager {

    /**
     * The pretty printer.
     */
    protected PrettyPrinter prettyPrinter;

    /**
     * The writer used to output the tokens.
     */
    protected Writer writer;

    /**
     * The indentation level.
     */
    protected int level;

    /**
     * The margin.
     */
    protected StringBuffer margin = new StringBuffer();

    /**
     * The current line.
     */
    protected int line = 1;

    /**
     * The current column.
     */
    protected int column;

    /**
     * The xml:space values.
     */
    protected List xmlSpace = new LinkedList();
    {
        xmlSpace.add(Boolean.FALSE);
    }

    /**
     * Whether the next markup can be indented.
     */
    protected boolean canIndent = true;

    /**
     * The elements starting lines.
     */
    protected List startingLines = new LinkedList();

    /**
     * Whether the attribute layout must be done on a single line.
     */
    protected boolean lineAttributes = false;

    /**
     * Creates a new output manager.
     * @param pp The PrettyPrinter used for formatting the output.
     * @param w The Writer to write the output to.
     */
    public OutputManager(PrettyPrinter pp, Writer w) {
        prettyPrinter = pp;
        writer = w;
    }

    /**
     * Prints a single character.
     */
    public void printCharacter(char c) throws IOException {
        if (c == 10) {
            printNewline();
        } else {
            column++;
            writer.write(c);
        }
    }

    /**
     * Prints a newline.
     */
    public void printNewline() throws IOException {
        String nl = prettyPrinter.getNewline();
        for (int i = 0; i < nl.length(); i++) {
            writer.write(nl.charAt(i));
        }
        column = 0;
        line++;
     }

    /**
     * Prints a string.
     */
    public void printString(String s) throws IOException {
        for (int i = 0; i < s.length(); i++) {
            printCharacter(s.charAt(i));
        }
    }

    /**
     * Prints a char array.
     */
    public void printCharacters(char[] ca) throws IOException {
        for (char aCa : ca) {
            printCharacter(aCa);
        }
    }

    /**
     * Prints white spaces.
     * @param text The space text.
     * @param opt whether the space is optional.
     */
    public void printSpaces(char[] text, boolean opt) throws IOException {
        if (prettyPrinter.getFormat()) {
            if (!opt) {
                printCharacter(' ');
            }
        } else {
            printCharacters(text);
        }
    }

    /**
     * Prints top level white spaces.
     * @param text The space text.
     */
    public void printTopSpaces(char[] text) throws IOException {
        if (prettyPrinter.getFormat()) {
            int nl = newlines(text);
            for (int i = 0; i < nl; i++) {
                printNewline();
            }
        } else {
            printCharacters(text);
        }
    }

    /**
     * Prints a comment.
     * @param text The comment text.
     */
    public void printComment(char[] text) throws IOException {
        if (prettyPrinter.getFormat()) {
            if (canIndent) {
                printNewline();
                printString(margin.toString());
            }
            printString("<!--");
            if (column + text.length + 3 < prettyPrinter.getDocumentWidth()) {
                printCharacters(text);
            } else {
                formatText(text, margin.toString(), false);
                printCharacter(' ');
            }
            if (column + 3 > prettyPrinter.getDocumentWidth()) {
                printNewline();
                printString(margin.toString());
            }
            printString("-->");
        } else {
            printString("<!--");
            printCharacters(text);
            printString("-->");
        }
    }

    /**
     * Prints an XML declaration.
     */
    public void printXMLDecl(char[] space1,
                             char[] space2,
                             char[] space3,
                             char[] version, char versionDelim,
                             char[] space4,
                             char[] space5,
                             char[] space6,
                             char[] encoding, char encodingDelim,
                             char[] space7,
                             char[] space8,
                             char[] space9,
                             char[] standalone, char standaloneDelim,
                             char[] space10)
        throws IOException {
        printString("<?xml");

        printSpaces(space1, false);

        printString("version");

        if (space2 != null) {
            printSpaces(space2, true);
        }

        printCharacter('=');

        if (space3 != null) {
            printSpaces(space3, true);
        }

        printCharacter(versionDelim);
        printCharacters(version);
        printCharacter(versionDelim);

        if (space4 != null) {
            printSpaces(space4, false);

            if (encoding != null) {
                printString("encoding");
            
                if (space5 != null) {
                    printSpaces(space5, true);
                }

                printCharacter('=');

                if (space6 != null) {
                    printSpaces(space6, true);
                }

                printCharacter(encodingDelim);
                printCharacters(encoding);
                printCharacter(encodingDelim);

                if (space7 != null) {
                    printSpaces(space7, standalone == null);
                }
            }

            if (standalone != null) {
                printString("standalone");
            
                if (space8 != null) {
                    printSpaces(space8, true);
                }

                printCharacter('=');

                if (space9 != null) {
                    printSpaces(space9, true);
                }

                printCharacter(standaloneDelim);
                printCharacters(standalone);
                printCharacter(standaloneDelim);

                if (space10 != null) {
                    printSpaces(space10, true);
                }
            }
        }

        printString("?>");
    }

    /**
     * Prints a processing instruction.
     */
    public void printPI(char[] target, char[] space, char[] data) throws IOException {
        if (prettyPrinter.getFormat()) {
            if (canIndent) {
                printNewline();
                printString(margin.toString());
            }
        }
        printString("<?");
        printCharacters(target);
        printSpaces(space, false);
        printCharacters(data);
        printString("?>");
    }

    /**
     * Prints the portion of the doctype before '['.
     */
    public void printDoctypeStart(char[] space1,
                                  char[] root,
                                  char[] space2,
                                  String externalId,
                                  char[] space3,
                                  char[] string1, char string1Delim,
                                  char[] space4,
                                  char[] string2, char string2Delim,
                                  char[] space5) throws IOException {
        if (prettyPrinter.getFormat()) {
            printString("<!DOCTYPE");

            printCharacter(' ');
            printCharacters(root);
        
            if (space2 != null) {
                printCharacter(' ');
                printString(externalId);
                printCharacter(' ');
            
                printCharacter(string1Delim);
                printCharacters(string1);
                printCharacter(string1Delim);

                if (space4 != null) {
                    if (string2 != null) {
                        if (column + string2.length + 3 >
                            prettyPrinter.getDocumentWidth()) {
                            printNewline();
                            for (int i = 0;
                                 i < prettyPrinter.getTabulationWidth();
                                 i++) {
                                printCharacter(' ');
                            }
                        } else {
                            printCharacter(' ');
                        }
                        printCharacter(string2Delim);
                        printCharacters(string2);
                        printCharacter(string2Delim);
                        printCharacter(' ');
                    }
                }
            }
        } else {
            printString("<!DOCTYPE");

            printSpaces(space1, false);
            printCharacters(root);
        
            if (space2 != null) {
                printSpaces(space2, false);
                printString(externalId);
                printSpaces(space3, false);
            
                printCharacter(string1Delim);
                printCharacters(string1);
                printCharacter(string1Delim);

                if (space4 != null) {
                    printSpaces(space4, string2 == null);

                    if (string2 != null) {
                        printCharacter(string2Delim);
                        printCharacters(string2);
                        printCharacter(string2Delim);
                        
                        if (space5 != null) {
                            printSpaces(space5, true);
                        }
                    }
                }
            }
        }
    }

    /**
     * Prints the portion of the doctype after ']'.
     */
    public void printDoctypeEnd(char[] space) throws IOException {
        if (space != null) {
            printSpaces(space, true);
        }
        printCharacter('>');
    }

    /**
     * Prints a parameter entity reference.
     */
    public void printParameterEntityReference(char[] name) throws IOException {
        printCharacter('%');
        printCharacters(name);
        printCharacter(';');
    }

    /**
     * Prints an entity reference.
     */
    public void printEntityReference(char[] name, 
                                     boolean first) throws IOException {
        if ((prettyPrinter.getFormat()) &&
            (xmlSpace.get(0) != Boolean.TRUE) &&
            first) {
            printNewline();
            printString(margin.toString());
        }
        printCharacter('&');
        printCharacters(name);
        printCharacter(';');
    }

    /**
     * Prints a character entity reference.
     */
    public void printCharacterEntityReference
        (char[] code, boolean first, boolean preceedingSpace) 
        throws IOException {
        if ((prettyPrinter.getFormat()) &&
            (xmlSpace.get(0) != Boolean.TRUE)) {

            if (first) {
                printNewline();
                printString(margin.toString());
            } else if (preceedingSpace) {
                int endCol = column + code.length + 3;
                if (endCol > prettyPrinter.getDocumentWidth()){
                    printNewline();
                    printString(margin.toString());
                } else {
                    printCharacter(' ');
                }
            }
        }
        printString("&#");
        printCharacters(code);
        printCharacter(';');
    }

    /**
     * Prints the start of an element.
     */
    public void printElementStart(char[] name, List attributes, char[] space)
        throws IOException {
        xmlSpace.add(0, xmlSpace.get(0));

        startingLines.add(0, line);

        if (prettyPrinter.getFormat()) {
            if (canIndent) {
                printNewline();
                printString(margin.toString());
            }
        }
        printCharacter('<');
        printCharacters(name);

        if (prettyPrinter.getFormat()) {
            Iterator it = attributes.iterator();
            if (it.hasNext()) {
                AttributeInfo ai = (AttributeInfo)it.next();

                if (ai.isAttribute("xml:space")) {
                    xmlSpace.set(0, (ai.value.equals("preserve")
                                     ? Boolean.TRUE
                                     : Boolean.FALSE));
                }
                
                printCharacter(' ');
                printCharacters(ai.name);
                printCharacter('=');
                printCharacter(ai.delimiter);
                printString(ai.value);
                printCharacter(ai.delimiter);
            }
            while (it.hasNext()) {
                AttributeInfo ai = (AttributeInfo)it.next();

                if (ai.isAttribute("xml:space")) {
                    xmlSpace.set(0, (ai.value.equals("preserve")
                                     ? Boolean.TRUE
                                     : Boolean.FALSE));
                }
                
                int len = ai.name.length + ai.value.length() + 4;
                if (lineAttributes ||
                    len + column > prettyPrinter.getDocumentWidth()) {
                    printNewline();
                    printString(margin.toString());
                    for (int i = 0; i < name.length + 2; i++) {
                        printCharacter(' ');
                    }
                } else {
                    printCharacter(' ');
                }
                printCharacters(ai.name);
                printCharacter('=');
                printCharacter(ai.delimiter);
                printString(ai.value);
                printCharacter(ai.delimiter);
            }
        } else {
            for (Object attribute : attributes) {
                AttributeInfo ai = (AttributeInfo) attribute;

                if (ai.isAttribute("xml:space")) {
                    xmlSpace.set(0, (ai.value.equals("preserve")
                            ? Boolean.TRUE
                            : Boolean.FALSE));
                }

                printSpaces(ai.space, false);
                printCharacters(ai.name);

                if (ai.space1 != null) {
                    printSpaces(ai.space1, true);
                }
                printCharacter('=');
                if (ai.space2 != null) {
                    printSpaces(ai.space2, true);
                }

                printCharacter(ai.delimiter);
                printString(ai.value);
                printCharacter(ai.delimiter);
            }
        }

        if (space != null) {
            printSpaces(space, true);
        }
        level++;
        for (int i = 0; i < prettyPrinter.getTabulationWidth(); i++) {
            margin.append(' ');
        }
        canIndent = true;
    }

    /**
     * Prints the end of an element.
     */
    public void printElementEnd(char[] name, char[] space) throws IOException {
        for (int i = 0; i < prettyPrinter.getTabulationWidth(); i++) {
            margin.deleteCharAt(0);
        }
        level--;
        if (name != null) {
            if (prettyPrinter.getFormat()) {
                if (xmlSpace.get(0) != Boolean.TRUE &&
                    (line != (Integer) startingLines.get(0) ||
                     column + name.length + 3 >= prettyPrinter.getDocumentWidth())) {
                    printNewline();
                    printString(margin.toString());
                }
            }
            printString("</");
            printCharacters(name);
            if (space != null) {
                printSpaces(space, true);
            }
            printCharacter('>');
        } else {
            printString("/>");
        }
        startingLines.remove(0);
        xmlSpace.remove(0);
    }

    /**
     * Prints the character data of an element content.
     */
    public boolean printCharacterData(char[] data, 
                                      boolean first,
                                      boolean preceedingSpace) 
        throws IOException {
        if (!prettyPrinter.getFormat()) {
            printCharacters(data);
            return false;
        }

        canIndent = true;
        if (isWhiteSpace(data)) {
            int nl = newlines(data);
            for (int i = 0; i < nl - 1; i++) {
                printNewline();
            }
            return true;
        }

        if (xmlSpace.get(0) == Boolean.TRUE) {
            printCharacters(data);
            canIndent = false;
            return false;
        }

        if (first) {
            printNewline();
            printString(margin.toString());
        }
        return formatText(data, margin.toString(), preceedingSpace);
    }

    /**
     * Prints a CDATA section.
     */
    public void printCDATASection(char[] data) throws IOException {
        printString("<![CDATA[");
        printCharacters(data);
        printString("]]>");
    }

    /**
     * Prints a notation declaration.
     */
    public void printNotation(char[] space1,
                              char[] name,
                              char[] space2,
                              String externalId,
                              char[] space3,
                              char[] string1, char string1Delim,
                              char[] space4,
                              char[] string2, char string2Delim,
                              char[] space5)
        throws IOException {
        writer.write("<!NOTATION");
        printSpaces(space1, false);
        writer.write(name);
        printSpaces(space2, false);
        writer.write(externalId);
        printSpaces(space3, false);

        writer.write(string1Delim);
        writer.write(string1);
        writer.write(string1Delim);
        
        if (space4 != null) {
            printSpaces(space4, false);

            if (string2 != null) {
                writer.write(string2Delim);
                writer.write(string2);
                writer.write(string2Delim);
            }
        }
        if (space5 != null) {
            printSpaces(space5, true);
        }
        writer.write('>');
    }
    
    /**
     * Prints an attribute list declaration start.
     */
    public void printAttlistStart(char[] space, char[] name) throws IOException {
        writer.write("<!ATTLIST");
        printSpaces(space, false);
        writer.write(name);
    }

    /**
     * Prints an attribute list declaration end.
     */
    public void printAttlistEnd(char[] space) throws IOException {
        if (space != null) {
            printSpaces(space, false);
        }
        writer.write('>');
    }

    /**
     * Prints an attribute declaration start.
     */
    public void printAttName(char[] space1, char[] name, char[] space2)
        throws IOException {
        printSpaces(space1, false);
        writer.write(name);
        printSpaces(space2, false);
    }

    /**
     * Prints an enumeration.
     */
    public void printEnumeration(List names) throws IOException {
        writer.write('(');

        Iterator it = names.iterator();
        NameInfo ni = (NameInfo)it.next();
        if (ni.space1 != null) {
            printSpaces(ni.space1, true);
        }
            
        writer.write(ni.name);

        if (ni.space2 != null) {
            printSpaces(ni.space2, true);
        }
        while (it.hasNext()) {
            writer.write('|');

            ni = (NameInfo)it.next();
            if (ni.space1 != null) {
                printSpaces(ni.space1, true);
            }
            
            writer.write(ni.name);

            if (ni.space2 != null) {
                printSpaces(ni.space2, true);
            }
        }

        writer.write(')');
    }

    /**
     * Returns the number of newlines in the given char array.
     */
    protected int newlines(char[] text) {
        int result = 0;
        for (char aText : text) {
            if (aText == 10) {
                result++;
            }
        }
        return result;
    }

    /**
     * Tells whether the given character represents white spaces.
     */
    protected boolean isWhiteSpace(char[] text) {
        for (char aText : text) {
            if (!XMLUtilities.isXMLSpace(aText)) {
                return false;
            }
        }
        return true;
    }

    /**
     * Formats the given text.
     */
    protected boolean formatText(char[] text, String margin,
                                 boolean preceedingSpace) throws IOException {
        int i = 0;
        boolean startsWithSpace = preceedingSpace;
        loop: while (i < text.length) {
            for (;;) {
                if (i >= text.length) {
                    break loop;
                }
                if (!XMLUtilities.isXMLSpace(text[i])) {
                    break;
                }
                startsWithSpace = true;
                i++;
            }
            StringBuffer sb = new StringBuffer();
            for (;;) {
                if (i >= text.length || XMLUtilities.isXMLSpace(text[i])) {
                    break;
                }
                sb.append(text[i++]);
            }
            if (sb.length() == 0) {
                return startsWithSpace;
            }
            if (startsWithSpace) {
                // Consider reformatting ws so things look nicer.
                int endCol = column + sb.length();
                if ((endCol >= prettyPrinter.getDocumentWidth() - 1) &&
                    ((margin.length() + sb.length() <
                      prettyPrinter.getDocumentWidth() - 1) ||
                     (margin.length() < column))) {
                    printNewline();
                    printString(margin);
                } else if (column > margin.length()) {
                    // Don't print space at start of new line.
                    printCharacter(' ');
                }
            }
            printString(sb.toString());
            startsWithSpace = false;
        }
        return startsWithSpace;
    }

    /**
     * To store the informations about a name.
     */
    public static class NameInfo {
        
        /**
         * The space before the name.
         */
        public char[] space1;

        /**
         * The name.
         */
        public char[] name;

        /**
         * The space after the name
         */
        public char[] space2;

        /**
         * Creates a new NameInfo.
         */
        public NameInfo(char[] sp1, char[] nm, char[] sp2) {
            space1 = sp1;
            name = nm;
            space2 = sp2;
        }
    }

    /**
     * To store the informations about an attribute.
     */
    public static class AttributeInfo {

        /**
         * The space before the name.
         */
        public char[] space;

        /**
         * The attribute name.
         */
        public char[] name;

        /**
         * The space before '='.
         */
        public char[] space1;

        /**
         * The space after '='.
         */
        public char[] space2;

        /**
         * The attribute value.
         */
        public String value;

        /**
         * The attribute value delimiter.
         */
        public char delimiter;

        /**
         * Whether the attribute value contains entity references.
         */
        public boolean entityReferences;

        /**
         * Creates a new AttributeInfo.
         */
        public AttributeInfo(char[] sp, char[] n, char[] sp1, char[] sp2,
                             String val, char delim, boolean entity) {
            space = sp;
            name = n;
            space1 = sp1;
            space2 = sp2;
            value = val;
            delimiter = delim;
            entityReferences = entity;
        }

        /**
         * Tells whether the name of the attribute represented by this class
         * equals the given string.
         */
        public boolean isAttribute(String s) {
            if (name.length == s.length()) {
                for (int i = 0; i < name.length; i++) {
                    if (name[i] != s.charAt(i)) {
                        return false;
                    }
                }
                return true;
            }
            return false;
        }
    }
}
