/**
 * 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.xml.security.c14n.implementations;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Set;

import javax.xml.parsers.ParserConfigurationException;

import org.apache.xml.security.c14n.CanonicalizationException;
import org.apache.xml.security.c14n.CanonicalizerSpi;
import org.apache.xml.security.c14n.helper.AttrCompare;
import org.apache.xml.security.signature.NodeFilter;
import org.apache.xml.security.signature.XMLSignatureInput;
import org.apache.xml.security.utils.Constants;
import org.apache.xml.security.utils.UnsyncByteArrayOutputStream;
import org.apache.xml.security.utils.XMLUtils;
import org.w3c.dom.Attr;
import org.w3c.dom.Comment;
import org.w3c.dom.DOMException;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.Node;
import org.w3c.dom.ProcessingInstruction;
import org.xml.sax.SAXException;

/**
 * Abstract base class for canonicalization algorithms.
 * Please note that these implementations are NOT thread safe - please see the following JIRA for more information:
 * https://issues.apache.org/jira/browse/SANTUARIO-463
 */
public abstract class CanonicalizerBase extends CanonicalizerSpi {
    public static final String XML = "xml";
    public static final String XMLNS = "xmlns";
    public static final String XMLNS_URI = Constants.NamespaceSpecNS;
    public static final String XML_LANG_URI = Constants.XML_LANG_SPACE_SpecNS;

    protected static final AttrCompare COMPARE = new AttrCompare();

    // Make sure you clone the following mutable arrays before passing to
    // potentially untrusted objects such as OutputStreams.
    private static final byte[] END_PI = {'?','>'};
    private static final byte[] BEGIN_PI = {'<','?'};
    private static final byte[] END_COMM = {'-','-','>'};
    private static final byte[] BEGIN_COMM = {'<','!','-','-'};
    private static final byte[] XA = {'&','#','x','A',';'};
    private static final byte[] X9 = {'&','#','x','9',';'};
    private static final byte[] QUOT = {'&','q','u','o','t',';'};
    private static final byte[] XD = {'&','#','x','D',';'};
    private static final byte[] GT = {'&','g','t',';'};
    private static final byte[] LT = {'&','l','t',';'};
    private static final byte[] END_TAG = {'<','/'};
    private static final byte[] AMP = {'&','a','m','p',';'};
    private static final byte[] EQUALS_STR = {'=','\"'};

    protected static final int NODE_BEFORE_DOCUMENT_ELEMENT = -1;
    protected static final int NODE_NOT_BEFORE_OR_AFTER_DOCUMENT_ELEMENT = 0;
    protected static final int NODE_AFTER_DOCUMENT_ELEMENT = 1;

    private List<NodeFilter> nodeFilter;

    private boolean includeComments;
    private Set<Node> xpathNodeSet;

    /**
     * The node to be skipped/excluded from the DOM tree
     * in subtree canonicalizations.
     */
    private Node excludeNode;
    private OutputStream writer = new ByteArrayOutputStream();

   /**
    * The null xmlns definition.
    */
    private Attr nullNode;

    /**
     * Constructor CanonicalizerBase
     *
     * @param includeComments
     */
    public CanonicalizerBase(boolean includeComments) {
        this.includeComments = includeComments;
    }

    /**
     * Method engineCanonicalizeSubTree
     * {@inheritDoc}
     * @param rootNode
     * @throws CanonicalizationException
     */
    public byte[] engineCanonicalizeSubTree(Node rootNode)
        throws CanonicalizationException {
        return engineCanonicalizeSubTree(rootNode, (Node)null);
    }

    /**
     * Method engineCanonicalizeXPathNodeSet
     * {@inheritDoc}
     * @param xpathNodeSet
     * @throws CanonicalizationException
     */
    public byte[] engineCanonicalizeXPathNodeSet(Set<Node> xpathNodeSet)
        throws CanonicalizationException {
        this.xpathNodeSet = xpathNodeSet;
        return engineCanonicalizeXPathNodeSetInternal(XMLUtils.getOwnerDocument(this.xpathNodeSet));
    }

    /**
     * Canonicalizes a Subtree node.
     * @param input the root of the subtree to canicalize
     * @return The canonicalize stream.
     * @throws CanonicalizationException
     */
    public byte[] engineCanonicalize(XMLSignatureInput input) throws CanonicalizationException {
        try {
            if (input.isExcludeComments()) {
                includeComments = false;
            }
            if (input.isOctetStream()) {
                return engineCanonicalize(input.getBytes());
            }
            if (input.isElement()) {
                return engineCanonicalizeSubTree(input.getSubNode(), input.getExcludeNode());
            } else if (input.isNodeSet()) {
                nodeFilter = input.getNodeFilters();

                circumventBugIfNeeded(input);

                if (input.getSubNode() != null) {
                    return engineCanonicalizeXPathNodeSetInternal(input.getSubNode());
                } else {
                    return engineCanonicalizeXPathNodeSet(input.getNodeSet());
                }
            }
            return null;
        } catch (ParserConfigurationException ex) {
            throw new CanonicalizationException(ex);
        } catch (IOException ex) {
            throw new CanonicalizationException(ex);
        } catch (SAXException ex) {
            throw new CanonicalizationException(ex);
        }
    }

    /**
     * @param writer The writer to set.
     */
    public void setWriter(OutputStream writer) {
        this.writer = writer;
    }

    protected OutputStream getWriter() {
        return writer;
    }

    /**
     * Canonicalizes a Subtree node.
     *
     * @param rootNode
     *            the root of the subtree to canonicalize
     * @param excludeNode
     *            a node to be excluded from the canonicalize operation
     * @return The canonicalize stream.
     * @throws CanonicalizationException
     */
    protected byte[] engineCanonicalizeSubTree(Node rootNode, Node excludeNode)
        throws CanonicalizationException {
        this.excludeNode = excludeNode;
        try {
            NameSpaceSymbTable ns = new NameSpaceSymbTable();
            int nodeLevel = NODE_BEFORE_DOCUMENT_ELEMENT;
            if (rootNode != null && Node.ELEMENT_NODE == rootNode.getNodeType()) {
                //Fills the nssymbtable with the definitions of the parent of the root subnode
                getParentNameSpaces((Element)rootNode, ns);
                nodeLevel = NODE_NOT_BEFORE_OR_AFTER_DOCUMENT_ELEMENT;
            }
            this.canonicalizeSubTree(rootNode, ns, rootNode, nodeLevel);
            this.writer.flush();
            if (this.writer instanceof ByteArrayOutputStream) {
                byte[] result = ((ByteArrayOutputStream)this.writer).toByteArray();
                if (reset) {
                    ((ByteArrayOutputStream)this.writer).reset();
                } else {
                    this.writer.close();
                }
                return result;
            } else if (this.writer instanceof UnsyncByteArrayOutputStream) {
                byte[] result = ((UnsyncByteArrayOutputStream)this.writer).toByteArray();
                if (reset) {
                    ((UnsyncByteArrayOutputStream)this.writer).reset();
                } else {
                    this.writer.close();
                }
                return result;
            } else {
                this.writer.close();
            }
            return null;

        } catch (UnsupportedEncodingException ex) {
            throw new CanonicalizationException(ex);
        } catch (IOException ex) {
            throw new CanonicalizationException(ex);
        }
    }


    /**
     * Method canonicalizeSubTree, this function is a recursive one.
     *
     * @param currentNode
     * @param ns
     * @param endnode
     * @throws CanonicalizationException
     * @throws IOException
     */
    protected final void canonicalizeSubTree(
        Node currentNode, NameSpaceSymbTable ns, Node endnode, int documentLevel
    ) throws CanonicalizationException, IOException {
        if (currentNode == null || isVisibleInt(currentNode) == -1) {
            return;
        }
        Node sibling = null;
        Node parentNode = null;
        final OutputStream writer = this.writer;
        final Node excludeNode = this.excludeNode;
        final boolean includeComments = this.includeComments;
        Map<String, byte[]> cache = new HashMap<>();
        do {
            switch (currentNode.getNodeType()) {

            case Node.ENTITY_NODE :
            case Node.NOTATION_NODE :
            case Node.ATTRIBUTE_NODE :
                // illegal node type during traversal
                throw new CanonicalizationException("empty",
                                                    new Object[]{"illegal node type during traversal"});

            case Node.DOCUMENT_FRAGMENT_NODE :
            case Node.DOCUMENT_NODE :
                ns.outputNodePush();
                sibling = currentNode.getFirstChild();
                break;

            case Node.COMMENT_NODE :
                if (includeComments) {
                    outputCommentToWriter((Comment) currentNode, writer, documentLevel);
                }
                break;

            case Node.PROCESSING_INSTRUCTION_NODE :
                outputPItoWriter((ProcessingInstruction) currentNode, writer, documentLevel);
                break;

            case Node.TEXT_NODE :
            case Node.CDATA_SECTION_NODE :
                outputTextToWriter(currentNode.getNodeValue(), writer);
                break;

            case Node.ELEMENT_NODE :
                documentLevel = NODE_NOT_BEFORE_OR_AFTER_DOCUMENT_ELEMENT;
                if (currentNode == excludeNode) {
                    break;
                }
                Element currentElement = (Element)currentNode;
                //Add a level to the nssymbtable. So latter can be pop-back.
                ns.outputNodePush();
                writer.write('<');
                String name = currentElement.getTagName();
                UtfHelpper.writeByte(name, writer, cache);

                outputAttributesSubtree(currentElement, ns, cache);

                writer.write('>');
                sibling = currentNode.getFirstChild();
                if (sibling == null) {
                    writer.write(END_TAG.clone());
                    UtfHelpper.writeStringToUtf8(name, writer);
                    writer.write('>');
                    //We finished with this level, pop to the previous definitions.
                    ns.outputNodePop();
                    if (parentNode != null) {
                        sibling = currentNode.getNextSibling();
                    }
                } else {
                    parentNode = currentElement;
                }
                break;

            case Node.DOCUMENT_TYPE_NODE :
            default :
                break;
            }
            while (sibling == null && parentNode != null) {
                writer.write(END_TAG.clone());
                UtfHelpper.writeByte(((Element)parentNode).getTagName(), writer, cache);
                writer.write('>');
                //We finished with this level, pop to the previous definitions.
                ns.outputNodePop();
                if (parentNode == endnode) {
                    return;
                }
                sibling = parentNode.getNextSibling();
                parentNode = parentNode.getParentNode();
                if (parentNode == null || Node.ELEMENT_NODE != parentNode.getNodeType()) {
                    documentLevel = NODE_AFTER_DOCUMENT_ELEMENT;
                    parentNode = null;
                }
            }
            if (sibling == null) {
                return;
            }
            currentNode = sibling;
            sibling = currentNode.getNextSibling();
        } while(true);
    }


    private byte[] engineCanonicalizeXPathNodeSetInternal(Node doc)
        throws CanonicalizationException {
        try {
            this.canonicalizeXPathNodeSet(doc, doc);
            this.writer.flush();
            if (this.writer instanceof ByteArrayOutputStream) {
                byte[] sol = ((ByteArrayOutputStream)this.writer).toByteArray();
                if (reset) {
                    ((ByteArrayOutputStream)this.writer).reset();
                } else {
                    this.writer.close();
                }
                return sol;
            } else if (this.writer instanceof UnsyncByteArrayOutputStream) {
                byte[] result = ((UnsyncByteArrayOutputStream)this.writer).toByteArray();
                if (reset) {
                    ((UnsyncByteArrayOutputStream)this.writer).reset();
                } else {
                    this.writer.close();
                }
                return result;
            } else {
                this.writer.close();
            }
            return null;
        } catch (UnsupportedEncodingException ex) {
            throw new CanonicalizationException(ex);
        } catch (IOException ex) {
            throw new CanonicalizationException(ex);
        }
    }

    /**
     * Canonicalizes all the nodes included in the currentNode and contained in the
     * xpathNodeSet field.
     *
     * @param currentNode
     * @param endnode
     * @throws CanonicalizationException
     * @throws IOException
     */
    protected final void canonicalizeXPathNodeSet(Node currentNode, Node endnode)
        throws CanonicalizationException, IOException {
        if (isVisibleInt(currentNode) == -1) {
            return;
        }
        boolean currentNodeIsVisible = false;
        NameSpaceSymbTable ns = new NameSpaceSymbTable();
        if (currentNode != null && Node.ELEMENT_NODE == currentNode.getNodeType()) {
            getParentNameSpaces((Element)currentNode, ns);
        }
        if (currentNode == null) {
            return;
        }
        Node sibling = null;
        Node parentNode = null;
        int documentLevel = NODE_BEFORE_DOCUMENT_ELEMENT;
        Map<String, byte[]> cache = new HashMap<>();
        do {
            switch (currentNode.getNodeType()) {

            case Node.ENTITY_NODE :
            case Node.NOTATION_NODE :
            case Node.ATTRIBUTE_NODE :
                // illegal node type during traversal
                throw new CanonicalizationException("empty",
                                                    new Object[]{"illegal node type during traversal"});

            case Node.DOCUMENT_FRAGMENT_NODE :
            case Node.DOCUMENT_NODE :
                ns.outputNodePush();
                sibling = currentNode.getFirstChild();
                break;

            case Node.COMMENT_NODE :
                if (this.includeComments && isVisibleDO(currentNode, ns.getLevel()) == 1) {
                    outputCommentToWriter((Comment) currentNode, writer, documentLevel);
                }
                break;

            case Node.PROCESSING_INSTRUCTION_NODE :
                if (isVisible(currentNode)) {
                    outputPItoWriter((ProcessingInstruction) currentNode, writer, documentLevel);
                }
                break;

            case Node.TEXT_NODE :
            case Node.CDATA_SECTION_NODE :
                if (isVisible(currentNode)) {
                    outputTextToWriter(currentNode.getNodeValue(), writer);
                    for (Node nextSibling = currentNode.getNextSibling();
                        nextSibling != null && (nextSibling.getNodeType() == Node.TEXT_NODE
                            || nextSibling.getNodeType() == Node.CDATA_SECTION_NODE);
                        nextSibling = nextSibling.getNextSibling()) {
                        outputTextToWriter(nextSibling.getNodeValue(), writer);
                        currentNode = nextSibling;
                        sibling = currentNode.getNextSibling();
                    }
                }
                break;

            case Node.ELEMENT_NODE :
                documentLevel = NODE_NOT_BEFORE_OR_AFTER_DOCUMENT_ELEMENT;
                Element currentElement = (Element) currentNode;
                //Add a level to the nssymbtable. So latter can be pop-back.
                String name = null;
                int i = isVisibleDO(currentNode, ns.getLevel());
                if (i == -1) {
                    sibling = currentNode.getNextSibling();
                    break;
                }
                currentNodeIsVisible = i == 1;
                if (currentNodeIsVisible) {
                    ns.outputNodePush();
                    writer.write('<');
                    name = currentElement.getTagName();
                    UtfHelpper.writeByte(name, writer, cache);
                } else {
                    ns.push();
                }

                outputAttributes(currentElement, ns, cache);

                if (currentNodeIsVisible) {
                    writer.write('>');
                }
                sibling = currentNode.getFirstChild();

                if (sibling == null) {
                    if (currentNodeIsVisible) {
                        writer.write(END_TAG.clone());
                        UtfHelpper.writeByte(name, writer, cache);
                        writer.write('>');
                        //We finished with this level, pop to the previous definitions.
                        ns.outputNodePop();
                    } else {
                        ns.pop();
                    }
                    if (parentNode != null) {
                        sibling = currentNode.getNextSibling();
                    }
                } else {
                    parentNode = currentElement;
                }
                break;

            case Node.DOCUMENT_TYPE_NODE :
            default :
                break;
            }
            while (sibling == null && parentNode != null) {
                if (isVisible(parentNode)) {
                    writer.write(END_TAG.clone());
                    UtfHelpper.writeByte(((Element)parentNode).getTagName(), writer, cache);
                    writer.write('>');
                    //We finished with this level, pop to the previous definitions.
                    ns.outputNodePop();
                } else {
                    ns.pop();
                }
                if (parentNode == endnode) {
                    return;
                }
                sibling = parentNode.getNextSibling();
                parentNode = parentNode.getParentNode();
                if (parentNode == null || Node.ELEMENT_NODE != parentNode.getNodeType()) {
                    parentNode = null;
                    documentLevel = NODE_AFTER_DOCUMENT_ELEMENT;
                }
            }
            if (sibling == null) {
                return;
            }
            currentNode = sibling;
            sibling = currentNode.getNextSibling();
        } while(true);
    }

    protected int isVisibleDO(Node currentNode, int level) {
        if (nodeFilter != null) {
            Iterator<NodeFilter> it = nodeFilter.iterator();
            while (it.hasNext()) {
                int i = it.next().isNodeIncludeDO(currentNode, level);
                if (i != 1) {
                    return i;
                }
            }
        }
        if (this.xpathNodeSet != null && !this.xpathNodeSet.contains(currentNode)) {
            return 0;
        }
        return 1;
    }

    protected int isVisibleInt(Node currentNode) {
        if (nodeFilter != null) {
            Iterator<NodeFilter> it = nodeFilter.iterator();
            while (it.hasNext()) {
                int i = it.next().isNodeInclude(currentNode);
                if (i != 1) {
                    return i;
                }
            }
        }
        if (this.xpathNodeSet != null && !this.xpathNodeSet.contains(currentNode)) {
            return 0;
        }
        return 1;
    }

    protected boolean isVisible(Node currentNode) {
        if (nodeFilter != null) {
            Iterator<NodeFilter> it = nodeFilter.iterator();
            while (it.hasNext()) {
                if (it.next().isNodeInclude(currentNode) != 1) {
                    return false;
                }
            }
        }
        if (this.xpathNodeSet != null && !this.xpathNodeSet.contains(currentNode)) {
            return false;
        }
        return true;
    }

    protected void handleParent(Element e, NameSpaceSymbTable ns) {
        if (!e.hasAttributes() && e.getNamespaceURI() == null) {
            return;
        }
        NamedNodeMap attrs = e.getAttributes();
        int attrsLength = attrs.getLength();
        for (int i = 0; i < attrsLength; i++) {
            Attr attribute = (Attr) attrs.item(i);
            String NName = attribute.getLocalName();
            String NValue = attribute.getNodeValue();

            if (Constants.NamespaceSpecNS.equals(attribute.getNamespaceURI())
                && (!XML.equals(NName) || !Constants.XML_LANG_SPACE_SpecNS.equals(NValue))) {
                ns.addMapping(NName, NValue, attribute);
            }
        }
        if (e.getNamespaceURI() != null) {
            String NName = e.getPrefix();
            String NValue = e.getNamespaceURI();
            String Name;
            if (NName == null || NName.equals("")) {
                NName = XMLNS;
                Name = XMLNS;
            } else {
                Name = XMLNS + ":" + NName;
            }
            Attr n = e.getOwnerDocument().createAttributeNS("http://www.w3.org/2000/xmlns/", Name);
            n.setValue(NValue);
            ns.addMapping(NName, NValue, n);
        }
    }

    /**
     * Adds to ns the definitions from the parent elements of el
     * @param el
     * @param ns
     */
    protected final void getParentNameSpaces(Element el, NameSpaceSymbTable ns)  {
        Node n1 = el.getParentNode();
        if (n1 == null || Node.ELEMENT_NODE != n1.getNodeType()) {
            return;
        }
        //Obtain all the parents of the element
        List<Element> parents = new ArrayList<>();
        Node parent = n1;
        while (parent != null && Node.ELEMENT_NODE == parent.getNodeType()) {
            parents.add((Element)parent);
            parent = parent.getParentNode();
        }
        //Visit them in reverse order.
        ListIterator<Element> it = parents.listIterator(parents.size());
        while (it.hasPrevious()) {
            Element ele = it.previous();
            handleParent(ele, ns);
        }
        parents.clear();
        Attr nsprefix = ns.getMappingWithoutRendered(XMLNS);
        if (nsprefix != null && "".equals(nsprefix.getValue())) {
            ns.addMappingAndRender(
                    XMLNS, "", getNullNode(nsprefix.getOwnerDocument()));
        }
    }

    /**
     * Output the attributes for this node in XPathNodeSet c14n.
     *
     * @param element
     * @param ns
     * @param cache
     * @throws CanonicalizationException, DOMException, IOException
     */
    abstract void outputAttributes(Element element, NameSpaceSymbTable ns, Map<String, byte[]> cache)
        throws CanonicalizationException, DOMException, IOException;

    /**
     * Output the attributes for this node in a Subtree c14n.
     *
     * @param element
     * @param ns
     * @param cache
     * @throws CanonicalizationException, DOMException, IOException
     */
    abstract void outputAttributesSubtree(Element element, NameSpaceSymbTable ns, Map<String, byte[]> cache)
        throws CanonicalizationException, DOMException, IOException;

    abstract void circumventBugIfNeeded(XMLSignatureInput input)
        throws CanonicalizationException, ParserConfigurationException, IOException, SAXException;

    /**
     * Outputs an Attribute to the internal Writer.
     *
     * The string value of the node is modified by replacing
     * <UL>
     * <LI>all ampersands with <CODE>&amp;amp;</CODE></LI>
     * <LI>all open angle brackets with <CODE>&amp;lt;</CODE></LI>
     * <LI>all quotation mark characters with <CODE>&amp;quot;</CODE></LI>
     * <LI>and the whitespace characters <CODE>#x9</CODE>, #xA, and #xD, with character
     * references. The character references are written in uppercase
     * hexadecimal with no leading zeroes (for example, <CODE>#xD</CODE> is represented
     * by the character reference <CODE>&amp;#xD;</CODE>)</LI>
     * </UL>
     *
     * @param name
     * @param value
     * @param writer
     * @throws IOException
     */
    protected static final void outputAttrToWriter(
        final String name, final String value,
        final OutputStream writer, final Map<String, byte[]> cache
    ) throws IOException {
        writer.write(' ');
        UtfHelpper.writeByte(name, writer, cache);
        writer.write(EQUALS_STR.clone());
        byte[] toWrite;
        final int length = value.length();
        int i = 0;
        while (i < length) {
            int c = value.codePointAt(i);
            i += Character.charCount(c);

            switch (c) {

            case '&' :
                toWrite = AMP.clone();
                break;

            case '<' :
                toWrite = LT.clone();
                break;

            case '"' :
                toWrite = QUOT.clone();
                break;

            case 0x09 :    // '\t'
                toWrite = X9.clone();
                break;

            case 0x0A :    // '\n'
                toWrite = XA.clone();
                break;

            case 0x0D :    // '\r'
                toWrite = XD.clone();
                break;

            default :
                if (c < 0x80) {
                    writer.write(c);
                } else {
                    UtfHelpper.writeCodePointToUtf8(c, writer);
                }
                continue;
            }
            writer.write(toWrite);
        }

        writer.write('\"');
    }

    /**
     * Outputs a PI to the internal Writer.
     *
     * @param currentPI
     * @param writer where to write the things
     * @throws IOException
     */
    protected void outputPItoWriter(
        ProcessingInstruction currentPI, OutputStream writer, int position
    ) throws IOException {
        if (position == NODE_AFTER_DOCUMENT_ELEMENT) {
            writer.write('\n');
        }
        writer.write(BEGIN_PI.clone());

        final String target = currentPI.getTarget();
        int length = target.length();

        for (int i = 0; i < length; ) {
            int c = target.codePointAt(i);
            i += Character.charCount(c);
            if (c == 0x0D) {
                writer.write(XD.clone());
            } else {
                if (c < 0x80) {
                    writer.write(c);
                } else {
                    UtfHelpper.writeCodePointToUtf8(c, writer);
                }
            }
        }

        final String data = currentPI.getData();

        length = data.length();

        if (length > 0) {
            writer.write(' ');

            for (int i = 0; i < length; ) {
                int c = data.codePointAt(i);
                i += Character.charCount(c);
                if (c == 0x0D) {
                    writer.write(XD.clone());
                } else {
                    UtfHelpper.writeCodePointToUtf8(c, writer);
                }
            }
        }

        writer.write(END_PI.clone());
        if (position == NODE_BEFORE_DOCUMENT_ELEMENT) {
            writer.write('\n');
        }
    }

    /**
     * Method outputCommentToWriter
     *
     * @param currentComment
     * @param writer writer where to write the things
     * @throws IOException
     */
    protected void outputCommentToWriter(
        Comment currentComment, OutputStream writer, int position
    ) throws IOException {
        if (position == NODE_AFTER_DOCUMENT_ELEMENT) {
            writer.write('\n');
        }
        writer.write(BEGIN_COMM.clone());

        final String data = currentComment.getData();
        final int length = data.length();

        for (int i = 0; i < length; ) {
            int c = data.codePointAt(i);
            i += Character.charCount(c);
            if (c == 0x0D) {
                writer.write(XD.clone());
            } else {
                if (c < 0x80) {
                    writer.write(c);
                } else {
                    UtfHelpper.writeCodePointToUtf8(c, writer);
                }
            }
        }

        writer.write(END_COMM.clone());
        if (position == NODE_BEFORE_DOCUMENT_ELEMENT) {
            writer.write('\n');
        }
    }

    /**
     * Outputs a Text of CDATA section to the internal Writer.
     *
     * @param text
     * @param writer writer where to write the things
     * @throws IOException
     */
    protected static final void outputTextToWriter(
        final String text, final OutputStream writer
    ) throws IOException {
        final int length = text.length();
        byte[] toWrite;
        for (int i = 0; i < length; ) {
            int c = text.codePointAt(i);
            i += Character.charCount(c);

            switch (c) {

            case '&' :
                toWrite = AMP.clone();
                break;

            case '<' :
                toWrite = LT.clone();
                break;

            case '>' :
                toWrite = GT.clone();
                break;

            case 0xD :
                toWrite = XD.clone();
                break;

            default :
                if (c < 0x80) {
                    writer.write(c);
                } else {
                    UtfHelpper.writeCodePointToUtf8(c, writer);
                }
                continue;
            }
            writer.write(toWrite);
        }
    }

    // The null xmlns definition.
    protected Attr getNullNode(Document ownerDocument) {
        if (nullNode == null) {
            try {
                nullNode = ownerDocument.createAttributeNS(
                                    Constants.NamespaceSpecNS, XMLNS);
                nullNode.setValue("");
            } catch (Exception e) {
                throw new RuntimeException("Unable to create nullNode: " + e);
            }
        }
        return nullNode;
    }

}
