/*

 ============================================================================
                   The Apache Software License, Version 1.1
 ============================================================================

 Copyright (C) 1999-2003 The Apache Software Foundation. All rights reserved.

 Redistribution and use in source and binary forms, with or without modifica-
 tion, are permitted provided that the following conditions are met:

 1. Redistributions of  source code must  retain the above copyright  notice,
    this list of conditions and the following disclaimer.

 2. Redistributions in binary form must reproduce the above copyright notice,
    this list of conditions and the following disclaimer in the documentation
    and/or other materials provided with the distribution.

 3. The end-user documentation included with the redistribution, if any, must
    include  the following  acknowledgment:  "This product includes  software
    developed  by the  Apache Software Foundation  (http://www.apache.org/)."
    Alternately, this  acknowledgment may  appear in the software itself,  if
    and wherever such third-party acknowledgments normally appear.

 4. The names "Batik" and  "Apache Software Foundation" must  not  be
    used to  endorse or promote  products derived from  this software without
    prior written permission. For written permission, please contact
    apache@apache.org.

 5. Products  derived from this software may not  be called "Apache", nor may
    "Apache" appear  in their name,  without prior written permission  of the
    Apache Software Foundation.

 THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESSED OR IMPLIED WARRANTIES,
 INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND
 FITNESS  FOR A PARTICULAR  PURPOSE ARE  DISCLAIMED.  IN NO  EVENT SHALL  THE
 APACHE SOFTWARE  FOUNDATION  OR ITS CONTRIBUTORS  BE LIABLE FOR  ANY DIRECT,
 INDIRECT, INCIDENTAL, SPECIAL,  EXEMPLARY, OR CONSEQUENTIAL  DAMAGES (INCLU-
 DING, BUT NOT LIMITED TO, PROCUREMENT  OF SUBSTITUTE GOODS OR SERVICES; LOSS
 OF USE, DATA, OR  PROFITS; OR BUSINESS  INTERRUPTION)  HOWEVER CAUSED AND ON
 ANY  THEORY OF LIABILITY,  WHETHER  IN CONTRACT,  STRICT LIABILITY,  OR TORT
 (INCLUDING  NEGLIGENCE OR  OTHERWISE) ARISING IN  ANY WAY OUT OF THE  USE OF
 THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

 This software  consists of voluntary contributions made  by many individuals
 on  behalf of the Apache Software  Foundation. For more  information on the
 Apache Software Foundation, please see <http://www.apache.org/>.

*/

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.7 2003/08/08 11:39:26 vhardy 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;
        }
    }
}
