/*****************************************************************************
 * Copyright (C) The Apache Software Foundation. All rights reserved.        *
 * ------------------------------------------------------------------------- *
 * This software is published under the terms of the Apache Software License *
 * version 1.1, a copy of which has been included with this distribution in  *
 * the LICENSE file.                                                         *
 *****************************************************************************/

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: OutputManager.java,v 1.6 2002/04/19 16:04:05 hillion Exp $
 */
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 s The scanner used for input tokenization.
     */
    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 (int i = 0; i < ca.length; i++) {
            printCharacter(ca[i]);
        }
    }

    /**
     * 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.
     * @param opt whether the space is optional.
     */
    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());
                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) throws IOException {
        printCharacter('&');
        printCharacters(name);
        printCharacter(';');
    }

    /**
     * Prints a character entity reference.
     */
    public void printCharacterEntityReference(char[] code) throws IOException {
        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, new Integer(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 {
            Iterator it = attributes.iterator();
            while (it.hasNext()) {
                AttributeInfo ai = (AttributeInfo)it.next();

                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)).intValue() ||
                     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 void printCharacterData(char[] data) throws IOException {
        if (prettyPrinter.getFormat()) {
            canIndent = true;
            if (isWhiteSpace(data)) {
                int nl = newlines(data);
                for (int i = 0; i < nl - 1; i++) {
                    printNewline();
                }
            } else {
                if (xmlSpace.get(0) == Boolean.TRUE) {
                    printCharacters(data);
                    canIndent = false;
                } else {
                    printNewline();
                    printString(margin.toString());
                    formatText(data, margin.toString());
                }
            }
        } else {
            printCharacters(data);
        }
    }

    /**
     * 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 (int i = 0; i < text.length; i++) {
            if (text[i] == 10) {
                result++;
            }
        }
        return result;
    }

    /**
     * Tells whether the given character represents white spaces.
     */
    protected boolean isWhiteSpace(char[] text) {
        for (int i = 0; i < text.length; i++) {
            if (!XMLUtilities.isXMLSpace(text[i])) {
                return false;
            }
        }
        return true;
    }

    /**
     * Formats the given text.
     */
    protected void formatText(char[] text, String margin) throws IOException {
        int i = 0;

        loop: while (i < text.length) {
            for (;;) {
                if (i >= text.length) {
                    break loop;
                }
                if (!XMLUtilities.isXMLSpace(text[i])) {
                    break;
                }
                i++;
            }
            StringBuffer sb = new StringBuffer();
            for (;;) {
                if (i >= text.length || XMLUtilities.isXMLSpace(text[i])) {
                    break;
                }
                sb.append(text[i++]);
            }
            if (sb.length() == 0) {
                break;
            }
            if (column + sb.length() >= prettyPrinter.getDocumentWidth() - 1) {
                if (margin.length() + sb.length() <
                    prettyPrinter.getDocumentWidth() - 1 ||
                    margin.length() < column) {
                    printNewline();
                    printString(margin);
                    printString(sb.toString());
                } else {
                    if (column >= margin.length()) {
                        printCharacter(' ');
                    }
                    printString(sb.toString());
                }
            } else {
                if (column > margin.length()) {
                    printCharacter(' ');
                }
                printString(sb.toString());
            }
        }
    }

    /**
     * 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;
        }
    }
}
