/* -*-             c-basic-offset: 4; indent-tabs-mode: nil; -*-  //------100-columns-wide------>|*/
// for license please see accompanying LICENSE.txt file (available also at http://www.xmlpull.org/)

package org.codehaus.plexus.util.xml.pull;

import java.io.IOException;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;

/**
 * Implementation of XmlSerializer interface from XmlPull V1 API. This implementation is optimized for performance and
 * low memory footprint.
 * <p>
 * Implemented features:
 * <ul>
 * <li>FEATURE_NAMES_INTERNED - when enabled all returned names (namespaces, prefixes) will be interned and it is
 * required that all names passed as arguments MUST be interned
 * <li>FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE
 * </ul>
 * <p>
 * Implemented properties:
 * <ul>
 * <li>PROPERTY_SERIALIZER_INDENTATION
 * <li>PROPERTY_SERIALIZER_LINE_SEPARATOR
 * </ul>
 * <p>C0n control characters except <code>\n</code>, <code>\r</code>, and <code>\t</code> are omitted from output</p>
 */
public class MXSerializer implements XmlSerializer {
    protected static final String XML_URI = "http://www.w3.org/XML/1998/namespace";

    protected static final String XMLNS_URI = "http://www.w3.org/2000/xmlns/";

    private static final boolean TRACE_SIZING = false;

    protected final String FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE =
            "http://xmlpull.org/v1/doc/features.html#serializer-attvalue-use-apostrophe";

    protected final String FEATURE_NAMES_INTERNED = "http://xmlpull.org/v1/doc/features.html#names-interned";

    protected final String PROPERTY_SERIALIZER_INDENTATION =
            "http://xmlpull.org/v1/doc/properties.html#serializer-indentation";

    protected final String PROPERTY_SERIALIZER_LINE_SEPARATOR =
            "http://xmlpull.org/v1/doc/properties.html#serializer-line-separator";

    protected static final String PROPERTY_LOCATION = "http://xmlpull.org/v1/doc/properties.html#location";

    // properties/features
    protected boolean namesInterned;

    protected boolean attributeUseApostrophe;

    protected String indentationString = null; // " ";

    protected String lineSeparator = "\n";

    protected String location;

    protected Writer out;

    protected int autoDeclaredPrefixes;

    protected int depth = 0;

    // element stack
    protected String elNamespace[] = new String[2];

    protected String elName[] = new String[elNamespace.length];

    protected int elNamespaceCount[] = new int[elNamespace.length];

    // namespace stack
    protected int namespaceEnd = 0;

    protected String namespacePrefix[] = new String[8];

    protected String namespaceUri[] = new String[namespacePrefix.length];

    protected boolean finished;

    protected boolean pastRoot;

    protected boolean setPrefixCalled;

    protected boolean startTagIncomplete;

    protected boolean doIndent;

    protected boolean seenTag;

    protected boolean seenBracket;

    protected boolean seenBracketBracket;

    // buffer output if needed to write escaped String see text(String)
    private static final int BUF_LEN = Runtime.getRuntime().freeMemory() > 1000000L ? 8 * 1024 : 256;

    protected char buf[] = new char[BUF_LEN];

    protected static final String precomputedPrefixes[];

    static {
        precomputedPrefixes = new String[32]; // arbitrary number ...
        for (int i = 0; i < precomputedPrefixes.length; i++) {
            precomputedPrefixes[i] = ("n" + i).intern();
        }
    }

    private boolean checkNamesInterned = false;

    private void checkInterning(String name) {
        if (namesInterned && name != name.intern()) {
            throw new IllegalArgumentException(
                    "all names passed as arguments must be interned" + "when NAMES INTERNED feature is enabled");
        }
    }

    protected void reset() {
        location = null;
        out = null;
        autoDeclaredPrefixes = 0;
        depth = 0;

        // nullify references on all levels to allow it to be GCed
        for (int i = 0; i < elNamespaceCount.length; i++) {
            elName[i] = null;
            elNamespace[i] = null;
            elNamespaceCount[i] = 2;
        }

        namespaceEnd = 0;

        // NOTE: no need to intern() as all literal strings and string-valued constant expressions
        // are interned. String literals are defined in 3.10.5 of the Java Language Specification
        // just checking ...
        // assert "xmlns" == "xmlns".intern();
        // assert XMLNS_URI == XMLNS_URI.intern();

        // TODO: how to prevent from reporting this namespace?
        // this is special namespace declared for consistency with XML infoset
        namespacePrefix[namespaceEnd] = "xmlns";
        namespaceUri[namespaceEnd] = XMLNS_URI;
        ++namespaceEnd;

        namespacePrefix[namespaceEnd] = "xml";
        namespaceUri[namespaceEnd] = XML_URI;
        ++namespaceEnd;

        finished = false;
        pastRoot = false;
        setPrefixCalled = false;
        startTagIncomplete = false;
        // doIntent is not changed
        seenTag = false;

        seenBracket = false;
        seenBracketBracket = false;
    }

    protected void ensureElementsCapacity() {
        final int elStackSize = elName.length;
        // assert (depth + 1) >= elName.length;
        // we add at least one extra slot ...
        final int newSize = (depth >= 7 ? 2 * depth : 8) + 2; // = lucky 7 + 1 //25
        if (TRACE_SIZING) {
            System.err.println(getClass().getName() + " elStackSize " + elStackSize + " ==> " + newSize);
        }
        final boolean needsCopying = elStackSize > 0;
        String[] arr = null;
        // reuse arr local variable slot
        arr = new String[newSize];
        if (needsCopying) System.arraycopy(elName, 0, arr, 0, elStackSize);
        elName = arr;
        arr = new String[newSize];
        if (needsCopying) System.arraycopy(elNamespace, 0, arr, 0, elStackSize);
        elNamespace = arr;

        final int[] iarr = new int[newSize];
        if (needsCopying) {
            System.arraycopy(elNamespaceCount, 0, iarr, 0, elStackSize);
        } else {
            // special initialization
            iarr[0] = 0;
        }
        elNamespaceCount = iarr;
    }

    protected void ensureNamespacesCapacity() { // int size) {
        // int namespaceSize = namespacePrefix != null ? namespacePrefix.length : 0;
        // assert (namespaceEnd >= namespacePrefix.length);

        // if(size >= namespaceSize) {
        // int newSize = size > 7 ? 2 * size : 8; // = lucky 7 + 1 //25
        final int newSize = namespaceEnd > 7 ? 2 * namespaceEnd : 8;
        if (TRACE_SIZING) {
            System.err.println(getClass().getName() + " namespaceSize " + namespacePrefix.length + " ==> " + newSize);
        }
        final String[] newNamespacePrefix = new String[newSize];
        final String[] newNamespaceUri = new String[newSize];
        if (namespacePrefix != null) {
            System.arraycopy(namespacePrefix, 0, newNamespacePrefix, 0, namespaceEnd);
            System.arraycopy(namespaceUri, 0, newNamespaceUri, 0, namespaceEnd);
        }
        namespacePrefix = newNamespacePrefix;
        namespaceUri = newNamespaceUri;

        // TODO use hashes for quick namespace->prefix lookups
        // if( ! allStringsInterned ) {
        // int[] newNamespacePrefixHash = new int[newSize];
        // if(namespacePrefixHash != null) {
        // System.arraycopy(
        // namespacePrefixHash, 0, newNamespacePrefixHash, 0, namespaceEnd);
        // }
        // namespacePrefixHash = newNamespacePrefixHash;
        // }
        // prefixesSize = newSize;
        // ////assert nsPrefixes.length > size && nsPrefixes.length == newSize
        // }
    }

    @Override
    public void setFeature(String name, boolean state) throws IllegalArgumentException, IllegalStateException {
        if (name == null) {
            throw new IllegalArgumentException("feature name can not be null");
        }
        if (FEATURE_NAMES_INTERNED.equals(name)) {
            namesInterned = state;
        } else if (FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals(name)) {
            attributeUseApostrophe = state;
        } else {
            throw new IllegalStateException("unsupported feature " + name);
        }
    }

    @Override
    public boolean getFeature(String name) throws IllegalArgumentException {
        if (name == null) {
            throw new IllegalArgumentException("feature name can not be null");
        }
        if (FEATURE_NAMES_INTERNED.equals(name)) {
            return namesInterned;
        } else if (FEATURE_SERIALIZER_ATTVALUE_USE_APOSTROPHE.equals(name)) {
            return attributeUseApostrophe;
        } else {
            return false;
        }
    }

    // precomputed variables to simplify writing indentation
    protected int offsetNewLine;

    protected int indentationJump;

    protected char[] indentationBuf;

    protected int maxIndentLevel;

    protected boolean writeLineSeparator; // should end-of-line be written

    protected boolean writeIndentation; // is indentation used?

    /**
     * For maximum efficiency when writing indents the required output is pre-computed This is internal function that
     * recomputes buffer after user requested changes.
     */
    protected void rebuildIndentationBuf() {
        if (doIndent == false) return;
        final int maxIndent = 65; // hardcoded maximum indentation size in characters
        int bufSize = 0;
        offsetNewLine = 0;
        if (writeLineSeparator) {
            offsetNewLine = lineSeparator.length();
            bufSize += offsetNewLine;
        }
        maxIndentLevel = 0;
        if (writeIndentation) {
            indentationJump = indentationString.length();
            maxIndentLevel = maxIndent / indentationJump;
            bufSize += maxIndentLevel * indentationJump;
        }
        if (indentationBuf == null || indentationBuf.length < bufSize) {
            indentationBuf = new char[bufSize + 8];
        }
        int bufPos = 0;
        if (writeLineSeparator) {
            for (int i = 0; i < lineSeparator.length(); i++) {
                indentationBuf[bufPos++] = lineSeparator.charAt(i);
            }
        }
        if (writeIndentation) {
            for (int i = 0; i < maxIndentLevel; i++) {
                for (int j = 0; j < indentationString.length(); j++) {
                    indentationBuf[bufPos++] = indentationString.charAt(j);
                }
            }
        }
    }

    // if(doIndent) writeIndent();
    protected void writeIndent() throws IOException {
        final int start = writeLineSeparator ? 0 : offsetNewLine;
        final int level = (depth > maxIndentLevel) ? maxIndentLevel : depth;
        out.write(indentationBuf, start, (level * indentationJump) + offsetNewLine);
    }

    @Override
    public void setProperty(String name, Object value) throws IllegalArgumentException, IllegalStateException {
        if (name == null) {
            throw new IllegalArgumentException("property name can not be null");
        }
        if (PROPERTY_SERIALIZER_INDENTATION.equals(name)) {
            indentationString = (String) value;
        } else if (PROPERTY_SERIALIZER_LINE_SEPARATOR.equals(name)) {
            lineSeparator = (String) value;
        } else if (PROPERTY_LOCATION.equals(name)) {
            location = (String) value;
        } else {
            throw new IllegalStateException("unsupported property " + name);
        }
        writeLineSeparator = lineSeparator != null && lineSeparator.length() > 0;
        writeIndentation = indentationString != null && indentationString.length() > 0;
        // optimize - do not write when nothing to write ...
        doIndent = indentationString != null && (writeLineSeparator || writeIndentation);
        // NOTE: when indentationString == null there is no indentation
        // (even though writeLineSeparator may be true ...)
        rebuildIndentationBuf();
        seenTag = false; // for consistency
    }

    @Override
    public Object getProperty(String name) throws IllegalArgumentException {
        if (name == null) {
            throw new IllegalArgumentException("property name can not be null");
        }
        if (PROPERTY_SERIALIZER_INDENTATION.equals(name)) {
            return indentationString;
        } else if (PROPERTY_SERIALIZER_LINE_SEPARATOR.equals(name)) {
            return lineSeparator;
        } else if (PROPERTY_LOCATION.equals(name)) {
            return location;
        } else {
            return null;
        }
    }

    private String getLocation() {
        return location != null ? " @" + location : "";
    }

    // this is special method that can be accessed directly to retrieve Writer serializer is using
    public Writer getWriter() {
        return out;
    }

    @Override
    public void setOutput(Writer writer) {
        reset();
        out = writer;
    }

    @Override
    public void setOutput(OutputStream os, String encoding) throws IOException {
        if (os == null) throw new IllegalArgumentException("output stream can not be null");
        reset();
        if (encoding != null) {
            out = new OutputStreamWriter(os, encoding);
        } else {
            out = new OutputStreamWriter(os);
        }
    }

    @Override
    public void startDocument(String encoding, Boolean standalone) throws IOException {
        char apos = attributeUseApostrophe ? '\'' : '"';
        if (attributeUseApostrophe) {
            out.write("<?xml version='1.0'");
        } else {
            out.write("<?xml version=\"1.0\"");
        }
        if (encoding != null) {
            out.write(" encoding=");
            out.write(apos);
            out.write(encoding);
            out.write(apos);
            // out.write('\'');
        }
        if (standalone != null) {
            out.write(" standalone=");
            out.write(apos);
            if (standalone) {
                out.write("yes");
            } else {
                out.write("no");
            }
            out.write(apos);
            // if(standalone.booleanValue()) {
            // out.write(" standalone='yes'");
            // } else {
            // out.write(" standalone='no'");
            // }
        }
        out.write("?>");
        if (writeLineSeparator) {
            out.write(lineSeparator);
        }
    }

    @Override
    public void endDocument() throws IOException {
        // close all unclosed tag;
        while (depth > 0) {
            endTag(elNamespace[depth], elName[depth]);
        }
        if (writeLineSeparator) {
            out.write(lineSeparator);
        }
        // assert depth == 0;
        // assert startTagIncomplete == false;
        finished = pastRoot = startTagIncomplete = true;
        out.flush();
    }

    @Override
    public void setPrefix(String prefix, String namespace) throws IOException {
        if (startTagIncomplete) closeStartTag();
        // assert prefix != null;
        // assert namespace != null;
        if (prefix == null) {
            prefix = "";
        }
        if (!namesInterned) {
            prefix = prefix.intern(); // will throw NPE if prefix==null
        } else if (checkNamesInterned) {
            checkInterning(prefix);
        } else if (prefix == null) {
            throw new IllegalArgumentException("prefix must be not null" + getLocation());
        }

        // check that prefix is not duplicated ...
        for (int i = elNamespaceCount[depth]; i < namespaceEnd; i++) {
            if (prefix == namespacePrefix[i]) {
                throw new IllegalStateException("duplicated prefix " + printable(prefix) + getLocation());
            }
        }

        if (!namesInterned) {
            namespace = namespace.intern();
        } else if (checkNamesInterned) {
            checkInterning(namespace);
        } else if (namespace == null) {
            throw new IllegalArgumentException("namespace must be not null" + getLocation());
        }

        if (namespaceEnd >= namespacePrefix.length) {
            ensureNamespacesCapacity();
        }
        namespacePrefix[namespaceEnd] = prefix;
        namespaceUri[namespaceEnd] = namespace;
        ++namespaceEnd;
        setPrefixCalled = true;
    }

    protected String lookupOrDeclarePrefix(String namespace) {
        return getPrefix(namespace, true);
    }

    @Override
    public String getPrefix(String namespace, boolean generatePrefix) {
        // assert namespace != null;
        if (!namesInterned) {
            // when String is interned we can do much faster namespace stack lookups ...
            namespace = namespace.intern();
        } else if (checkNamesInterned) {
            checkInterning(namespace);
            // assert namespace != namespace.intern();
        }
        if (namespace == null) {
            throw new IllegalArgumentException("namespace must be not null" + getLocation());
        } else if (namespace.length() == 0) {
            throw new IllegalArgumentException("default namespace cannot have prefix" + getLocation());
        }

        // first check if namespace is already in scope
        for (int i = namespaceEnd - 1; i >= 0; --i) {
            if (namespace == namespaceUri[i]) {
                final String prefix = namespacePrefix[i];
                // now check that prefix is still in scope
                for (int p = namespaceEnd - 1; p > i; --p) {
                    if (prefix == namespacePrefix[p])
                        continue; // too bad - prefix is redeclared with different namespace
                }
                return prefix;
            }
        }

        // so not found it ...
        if (!generatePrefix) {
            return null;
        }
        return generatePrefix(namespace);
    }

    private String generatePrefix(String namespace) {
        // assert namespace == namespace.intern();
        while (true) {
            ++autoDeclaredPrefixes;
            // fast lookup uses table that was pre-initialized in static{} ....
            final String prefix = autoDeclaredPrefixes < precomputedPrefixes.length
                    ? precomputedPrefixes[autoDeclaredPrefixes]
                    : ("n" + autoDeclaredPrefixes).intern();
            // make sure this prefix is not declared in any scope (avoid hiding in-scope prefixes)!
            for (int i = namespaceEnd - 1; i >= 0; --i) {
                if (prefix == namespacePrefix[i]) {
                    continue; // prefix is already declared - generate new and try again
                }
            }
            // declare prefix

            if (namespaceEnd >= namespacePrefix.length) {
                ensureNamespacesCapacity();
            }
            namespacePrefix[namespaceEnd] = prefix;
            namespaceUri[namespaceEnd] = namespace;
            ++namespaceEnd;

            return prefix;
        }
    }

    @Override
    public int getDepth() {
        return depth;
    }

    @Override
    public String getNamespace() {
        return elNamespace[depth];
    }

    @Override
    public String getName() {
        return elName[depth];
    }

    @Override
    public XmlSerializer startTag(String namespace, String name) throws IOException {

        if (startTagIncomplete) {
            closeStartTag();
        }
        seenBracket = seenBracketBracket = false;
        if (doIndent && depth > 0 && seenTag) {
            writeIndent();
        }
        seenTag = true;
        setPrefixCalled = false;
        startTagIncomplete = true;
        ++depth;
        if ((depth + 1) >= elName.length) {
            ensureElementsCapacity();
        }
        //// assert namespace != null;

        if (checkNamesInterned && namesInterned) checkInterning(namespace);
        elNamespace[depth] = (namesInterned || namespace == null) ? namespace : namespace.intern();
        // assert name != null;
        // elName[ depth ] = name;
        if (checkNamesInterned && namesInterned) checkInterning(name);
        elName[depth] = (namesInterned || name == null) ? name : name.intern();
        if (out == null) {
            throw new IllegalStateException("setOutput() must called set before serialization can start");
        }
        out.write('<');
        if (namespace != null) {

            if (namespace.length() > 0) {
                // ALEK: in future make it as feature on serializer
                String prefix = null;
                if (depth > 0 && (namespaceEnd - elNamespaceCount[depth - 1]) == 1) {
                    // if only one prefix was declared un-declare it if prefix is already declared on parent el with the
                    // same URI
                    String uri = namespaceUri[namespaceEnd - 1];
                    if (uri == namespace || uri.equals(namespace)) {
                        String elPfx = namespacePrefix[namespaceEnd - 1];
                        // 2 == to skip predefined namespaces (xml and xmlns ...)
                        for (int pos = elNamespaceCount[depth - 1] - 1; pos >= 2; --pos) {
                            String pf = namespacePrefix[pos];
                            if (pf == elPfx || pf.equals(elPfx)) {
                                String n = namespaceUri[pos];
                                if (n == uri || n.equals(uri)) {
                                    --namespaceEnd; // un-declare namespace
                                    prefix = elPfx;
                                }
                                break;
                            }
                        }
                    }
                }
                if (prefix == null) {
                    prefix = lookupOrDeclarePrefix(namespace);
                }
                // assert prefix != null;
                // make sure that default ("") namespace to not print ":"
                if (prefix.length() > 0) {
                    out.write(prefix);
                    out.write(':');
                }
            } else {
                // make sure that default namespace can be declared
                for (int i = namespaceEnd - 1; i >= 0; --i) {
                    if (namespacePrefix[i] == "") {
                        final String uri = namespaceUri[i];
                        if (uri == null) {
                            // declare default namespace
                            setPrefix("", "");
                        } else if (uri.length() > 0) {
                            throw new IllegalStateException("start tag can not be written in empty default namespace "
                                    + "as default namespace is currently bound to '" + uri + "'" + getLocation());
                        }
                        break;
                    }
                }
            }
        }
        out.write(name);
        return this;
    }

    @Override
    public XmlSerializer attribute(String namespace, String name, String value) throws IOException {
        if (!startTagIncomplete) {
            throw new IllegalArgumentException("startTag() must be called before attribute()" + getLocation());
        }
        // assert setPrefixCalled == false;
        out.write(' ');
        //// assert namespace != null;
        if (namespace != null && namespace.length() > 0) {
            // namespace = namespace.intern();
            if (!namesInterned) {
                namespace = namespace.intern();
            } else if (checkNamesInterned) {
                checkInterning(namespace);
            }
            String prefix = lookupOrDeclarePrefix(namespace);
            // assert( prefix != null);
            if (prefix.length() == 0) {
                // needs to declare prefix to hold default namespace
                // NOTE: attributes such as a='b' are in NO namespace
                prefix = generatePrefix(namespace);
            }
            out.write(prefix);
            out.write(':');
            // if(prefix.length() > 0) {
            // out.write(prefix);
            // out.write(':');
            // }
        }
        // assert name != null;
        out.write(name);
        out.write('=');
        // assert value != null;
        out.write(attributeUseApostrophe ? '\'' : '"');
        writeAttributeValue(value, out);
        out.write(attributeUseApostrophe ? '\'' : '"');
        return this;
    }

    protected void closeStartTag() throws IOException {
        if (finished) {
            throw new IllegalArgumentException("trying to write past already finished output" + getLocation());
        }
        if (seenBracket) {
            seenBracket = seenBracketBracket = false;
        }
        if (startTagIncomplete || setPrefixCalled) {
            if (setPrefixCalled) {
                throw new IllegalArgumentException(
                        "startTag() must be called immediately after setPrefix()" + getLocation());
            }
            if (!startTagIncomplete) {
                throw new IllegalArgumentException("trying to close start tag that is not opened" + getLocation());
            }

            // write all namespace declarations!
            writeNamespaceDeclarations();
            out.write('>');
            elNamespaceCount[depth] = namespaceEnd;
            startTagIncomplete = false;
        }
    }

    private void writeNamespaceDeclarations() throws IOException {
        // int start = elNamespaceCount[ depth - 1 ];
        for (int i = elNamespaceCount[depth - 1]; i < namespaceEnd; i++) {
            if (doIndent && namespaceUri[i].length() > 40) {
                writeIndent();
                out.write(" ");
            }
            if (namespacePrefix[i] != "") {
                out.write(" xmlns:");
                out.write(namespacePrefix[i]);
                out.write('=');
            } else {
                out.write(" xmlns=");
            }
            out.write(attributeUseApostrophe ? '\'' : '"');

            // NOTE: escaping of namespace value the same way as attributes!!!!
            writeAttributeValue(namespaceUri[i], out);

            out.write(attributeUseApostrophe ? '\'' : '"');
        }
    }

    @Override
    public XmlSerializer endTag(String namespace, String name) throws IOException {
        // check that level is valid
        //// assert namespace != null;
        // if(namespace != null) {
        // namespace = namespace.intern();
        // }
        seenBracket = seenBracketBracket = false;
        if (namespace != null) {
            if (!namesInterned) {
                namespace = namespace.intern();
            } else if (checkNamesInterned) {
                checkInterning(namespace);
            }
        }

        if (namespace != elNamespace[depth]) {
            throw new IllegalArgumentException("expected namespace " + printable(elNamespace[depth]) + " and not "
                    + printable(namespace) + getLocation());
        }
        if (name == null) {
            throw new IllegalArgumentException("end tag name can not be null" + getLocation());
        }
        if (checkNamesInterned && namesInterned) {
            checkInterning(name);
        }

        if ((!namesInterned && !name.equals(elName[depth])) || (namesInterned && name != elName[depth])) {
            throw new IllegalArgumentException("expected element name " + printable(elName[depth]) + " and not "
                    + printable(name) + getLocation());
        }
        if (startTagIncomplete) {
            writeNamespaceDeclarations();
            out.write(" />"); // space is added to make it easier to work in XHTML!!!
            --depth;
        } else {
            --depth;
            // assert startTagIncomplete == false;
            if (doIndent && seenTag) {
                writeIndent();
            }
            out.write("</");
            if (namespace != null && namespace.length() > 0) {
                // TODO prefix should be already known from matching start tag ...
                final String prefix = lookupOrDeclarePrefix(namespace);
                // assert( prefix != null);
                if (prefix.length() > 0) {
                    out.write(prefix);
                    out.write(':');
                }
            }
            out.write(name);
            out.write('>');
        }
        namespaceEnd = elNamespaceCount[depth];
        startTagIncomplete = false;
        seenTag = true;
        return this;
    }

    @Override
    public XmlSerializer text(String text) throws IOException {
        // assert text != null;
        if (startTagIncomplete || setPrefixCalled) closeStartTag();
        if (doIndent && seenTag) seenTag = false;
        writeElementContent(text, out);
        return this;
    }

    @Override
    public XmlSerializer text(char[] buf, int start, int len) throws IOException {
        if (startTagIncomplete || setPrefixCalled) closeStartTag();
        if (doIndent && seenTag) seenTag = false;
        writeElementContent(buf, start, len, out);
        return this;
    }

    @Override
    public void cdsect(String text) throws IOException {
        if (startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
        if (doIndent && seenTag) seenTag = false;
        out.write("<![CDATA[");
        out.write(text); // escape?
        out.write("]]>");
    }

    @Override
    public void entityRef(String text) throws IOException {
        if (startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
        if (doIndent && seenTag) seenTag = false;
        out.write('&');
        out.write(text); // escape?
        out.write(';');
    }

    @Override
    public void processingInstruction(String text) throws IOException {
        if (startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
        if (doIndent && seenTag) seenTag = false;
        out.write("<?");
        out.write(text); // escape?
        out.write("?>");
    }

    @Override
    public void comment(String text) throws IOException {
        if (startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
        if (doIndent && seenTag) seenTag = false;
        out.write("<!--");
        out.write(text); // escape?
        out.write("-->");
    }

    @Override
    public void docdecl(String text) throws IOException {
        if (startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
        if (doIndent && seenTag) seenTag = false;
        out.write("<!DOCTYPE ");
        out.write(text); // escape?
        out.write(">");
    }

    @Override
    public void ignorableWhitespace(String text) throws IOException {
        if (startTagIncomplete || setPrefixCalled || seenBracket) closeStartTag();
        if (doIndent && seenTag) seenTag = false;
        if (text.length() == 0) {
            throw new IllegalArgumentException("empty string is not allowed for ignorable whitespace" + getLocation());
        }
        out.write(text); // no escape?
    }

    @Override
    public void flush() throws IOException {
        if (!finished && startTagIncomplete) closeStartTag();
        out.flush();
    }

    // --- utility methods

    protected void writeAttributeValue(String value, Writer out) throws IOException {
        if (value == null) {
            return;
        }
        // .[apostrophe and <, & escaped],
        final char quot = attributeUseApostrophe ? '\'' : '"';
        final String quotEntity = attributeUseApostrophe ? "&apos;" : "&quot;";

        int pos = 0;
        for (int i = 0; i < value.length(); i++) {
            char ch = value.charAt(i);
            if (ch == '&') {
                if (i > pos) out.write(value.substring(pos, i));
                out.write("&amp;");
                pos = i + 1;
            }
            if (ch == '<') {
                if (i > pos) out.write(value.substring(pos, i));
                out.write("&lt;");
                pos = i + 1;
            } else if (ch == quot) {
                if (i > pos) out.write(value.substring(pos, i));
                out.write(quotEntity);
                pos = i + 1;
            } else if (ch < 32) {
                // in XML 1.0 only legal character are #x9 | #xA | #xD
                // and they must be escaped otherwise in attribute value they are normalized to spaces
                if (ch == 13 || ch == 10 || ch == 9) {
                    if (i > pos) out.write(value.substring(pos, i));
                    out.write("&#");
                    out.write(Integer.toString(ch));
                    out.write(';');
                    pos = i + 1;
                } else {
                    throw new IllegalStateException(
                            "character " + Integer.toString(ch) + " is not allowed in output" + getLocation());
                    // in XML 1.1 legal are [#x1-#xD7FF]
                    // if(ch > 0) {
                    // if(i > pos) out.write(text.substring(pos, i));
                    // out.write("&#");
                    // out.write(Integer.toString(ch));
                    // out.write(';');
                    // pos = i + 1;
                    // } else {
                    // throw new IllegalStateException(
                    // "character zero is not allowed in XML 1.1 output"+getLocation());
                    // }
                }
            }
        }
        if (pos > 0) {
            out.write(value.substring(pos));
        } else {
            out.write(value); // this is shortcut to the most common case
        }
    }

    protected void writeElementContent(String text, Writer out) throws IOException {
        if (text == null) {
            return;
        }
        // escape '<', '&', ']]>', <32 if necessary
        int pos = 0;
        for (int i = 0; i < text.length(); i++) {
            // TODO: check if doing char[] text.getChars() would be faster than getCharAt(i) ...
            char ch = text.charAt(i);
            if (ch == ']') {
                if (seenBracket) {
                    seenBracketBracket = true;
                } else {
                    seenBracket = true;
                }
            } else {
                if (ch == '&') {
                    if (i > pos) out.write(text.substring(pos, i));
                    out.write("&amp;");
                    pos = i + 1;
                } else if (ch == '<') {
                    if (i > pos) out.write(text.substring(pos, i));
                    out.write("&lt;");
                    pos = i + 1;
                } else if (seenBracketBracket && ch == '>') {
                    if (i > pos) out.write(text.substring(pos, i));
                    out.write("&gt;");
                    pos = i + 1;
                } else if (ch < 32) {
                    // in XML 1.0 only legal character are #x9 | #xA | #xD
                    if (ch == 9 || ch == 10 || ch == 13) {
                        // pass through

                        // } else if(ch == 13) { //escape
                        // if(i > pos) out.write(text.substring(pos, i));
                        // out.write("&#");
                        // out.write(Integer.toString(ch));
                        // out.write(';');
                        // pos = i + 1;
                    } else {
                        // skip special char
                        if (i > pos) out.write(text.substring(pos, i));
                        pos = i + 1;
                    }
                }
                if (seenBracket) {
                    seenBracketBracket = seenBracket = false;
                }
            }
        }
        if (pos > 0) {
            out.write(text.substring(pos));
        } else {
            out.write(text); // this is shortcut to the most common case
        }
    }

    protected void writeElementContent(char[] buf, int off, int len, Writer out) throws IOException {
        // escape '<', '&', ']]>'
        final int end = off + len;
        int pos = off;
        for (int i = off; i < end; i++) {
            final char ch = buf[i];
            if (ch == ']') {
                if (seenBracket) {
                    seenBracketBracket = true;
                } else {
                    seenBracket = true;
                }
            } else {
                if (ch == '&') {
                    if (i > pos) {
                        out.write(buf, pos, i - pos);
                    }
                    out.write("&amp;");
                    pos = i + 1;
                } else if (ch == '<') {
                    if (i > pos) {
                        out.write(buf, pos, i - pos);
                    }
                    out.write("&lt;");
                    pos = i + 1;

                } else if (seenBracketBracket && ch == '>') {
                    if (i > pos) {
                        out.write(buf, pos, i - pos);
                    }
                    out.write("&gt;");
                    pos = i + 1;
                } else if (ch < 32) {
                    // in XML 1.0 only legal character are #x9 | #xA | #xD
                    if (ch == 9 || ch == 10 || ch == 13) {
                        // pass through

                        // } else if(ch == 13 ) { //if(ch == '\r') {
                        // if(i > pos) {
                        // out.write(buf, pos, i - pos);
                        // }
                        // out.write("&#");
                        // out.write(Integer.toString(ch));
                        // out.write(';');
                        // pos = i + 1;
                    } else {
                        throw new IllegalStateException(
                                "character " + Integer.toString(ch) + " is not allowed in output" + getLocation());
                        // in XML 1.1 legal are [#x1-#xD7FF]
                        // if(ch > 0) {
                        // if(i > pos) out.write(text.substring(pos, i));
                        // out.write("&#");
                        // out.write(Integer.toString(ch));
                        // out.write(';');
                        // pos = i + 1;
                        // } else {
                        // throw new IllegalStateException(
                        // "character zero is not allowed in XML 1.1 output"+getLocation());
                        // }
                    }
                }
                if (seenBracket) {
                    seenBracketBracket = seenBracket = false;
                }
                // assert seenBracketBracket == seenBracket == false;
            }
        }
        if (end > pos) {
            out.write(buf, pos, end - pos);
        }
    }

    // simple utility method -- good for debugging
    protected static final String printable(String s) {
        if (s == null) return "null";
        StringBuilder retval = new StringBuilder(s.length() + 16);
        retval.append("'");
        char ch;
        for (int i = 0; i < s.length(); i++) {
            addPrintable(retval, s.charAt(i));
        }
        retval.append("'");
        return retval.toString();
    }

    protected static final String printable(char ch) {
        StringBuilder retval = new StringBuilder();
        addPrintable(retval, ch);
        return retval.toString();
    }

    private static void addPrintable(StringBuilder retval, char ch) {
        switch (ch) {
            case '\b':
                retval.append("\\b");
                break;
            case '\t':
                retval.append("\\t");
                break;
            case '\n':
                retval.append("\\n");
                break;
            case '\f':
                retval.append("\\f");
                break;
            case '\r':
                retval.append("\\r");
                break;
            case '\"':
                retval.append("\\\"");
                break;
            case '\'':
                retval.append("\\\'");
                break;
            case '\\':
                retval.append("\\\\");
                break;
            default:
                if (ch < 0x20 || ch > 0x7e) {
                    final String ss = "0000" + Integer.toString(ch, 16);
                    retval.append("\\u").append(ss, ss.length() - 4, ss.length());
                } else {
                    retval.append(ch);
                }
        }
    }
}
