/*   Copyright 2004 The Apache Software Foundation
 *
 *   Licensed 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.xmlbeans.impl.store;

import org.apache.xmlbeans.SystemProperties;
import org.apache.xmlbeans.XmlDocumentProperties;
import org.apache.xmlbeans.XmlOptionCharEscapeMap;
import org.apache.xmlbeans.XmlOptions;
import org.apache.xmlbeans.impl.common.EncodingMap;
import org.apache.xmlbeans.impl.common.QNameHelper;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;

import javax.xml.namespace.QName;
import java.io.*;
import java.util.*;

abstract class Saver {
    static final int ROOT = Cur.ROOT;
    static final int ELEM = Cur.ELEM;
    static final int ATTR = Cur.ATTR;
    static final int COMMENT = Cur.COMMENT;
    static final int PROCINST = Cur.PROCINST;
    static final int TEXT = Cur.TEXT;


    private final Locale _locale;
    private final long _version;

    private SaveCur _cur;

    private List<String> _ancestorNamespaces;
    private final Map<String, String> _suggestedPrefixes;
    protected XmlOptionCharEscapeMap _replaceChar;
    private final boolean _useDefaultNamespace;
    private Map<String, String> _preComputedNamespaces;
    private final boolean _saveNamespacesFirst;

    private final ArrayList<QName> _attrNames = new ArrayList<>();
    private final ArrayList<String> _attrValues = new ArrayList<>();

    private final ArrayList<String> _namespaceStack = new ArrayList<>();
    private int _currentMapping;
    private final HashMap<String, String> _uriMap = new HashMap<>();
    private final HashMap<String, String> _prefixMap = new HashMap<>();
    private String _initialDefaultUri;

    static final String _newLine = SystemProperties.getProperty("line.separator", "\n");


    protected abstract boolean emitElement(SaveCur c, List<QName> attrNames, List<String> attrValues);

    protected abstract void emitFinish(SaveCur c);

    protected abstract void emitText(SaveCur c);

    protected abstract void emitComment(SaveCur c);

    protected abstract void emitProcinst(SaveCur c);

    protected abstract void emitDocType(String docTypeName, String publicId, String systemId);

    protected abstract void emitStartDoc(SaveCur c);

    protected abstract void emitEndDoc(SaveCur c);

    protected void syntheticNamespace(String prefix, String uri, boolean considerDefault) {
    }

    Saver(Cur c, XmlOptions options) {
        assert c._locale.entered();

        options = XmlOptions.maskNull(options);

        _cur = createSaveCur(c, options);

        _locale = c._locale;
        _version = _locale.version();

        // Define implicit xml prefixed namespace

        addMapping("xml", Locale._xml1998Uri);

        Map<String, String> m = options.getSaveImplicitNamespaces();
        if (m != null) {
            for (String prefix : m.keySet()) {
                addMapping(prefix, m.get(prefix));
            }
        }

        // define character map for escaped replacements
        _replaceChar = options.getSaveSubstituteCharacters();

        // If the default prefix has not been mapped, do so now

        if (getNamespaceForPrefix("") == null) {
            _initialDefaultUri = "";
            addMapping("", _initialDefaultUri);
        }

        if (options.isSaveAggressiveNamespaces() && !(this instanceof SynthNamespaceSaver)) {
            SynthNamespaceSaver saver = new SynthNamespaceSaver(c, options);

            //noinspection StatementWithEmptyBody
            while (saver.process()) {
            }

            if (!saver._synthNamespaces.isEmpty()) {
                _preComputedNamespaces = saver._synthNamespaces;
            }
        }

        _useDefaultNamespace = options.isUseDefaultNamespace();

        _saveNamespacesFirst = options.isSaveNamespacesFirst();


        _suggestedPrefixes = options.getSaveSuggestedPrefixes();

        _ancestorNamespaces = _cur.getAncestorNamespaces();
    }

    private static SaveCur createSaveCur(Cur c, XmlOptions options) {
        QName synthName = options.getSaveSyntheticDocumentElement();

        QName fragName = synthName;

        if (fragName == null) {
            fragName = options.isSaveUseOpenFrag()
                ? Locale._openuriFragment
                : Locale._xmlFragment;
        }

        boolean saveInner = options.isSaveInner() && !options.isSaveOuter();

        Cur start = c.tempCur();
        Cur end = c.tempCur();

        SaveCur cur = null;

        int k = c.kind();

        switch (k) {
            case ROOT: {
                positionToInner(c, start, end);

                if (Locale.isFragment(start, end)) {
                    cur = new FragSaveCur(start, end, fragName);
                } else if (synthName != null) {
                    cur = new FragSaveCur(start, end, synthName);
                } else {
                    cur = new DocSaveCur(c);
                }

                break;
            }

            case ELEM: {
                if (saveInner) {
                    positionToInner(c, start, end);

                    cur =
                        new FragSaveCur(
                            start, end, Locale.isFragment(start, end) ? fragName : synthName);
                } else if (synthName != null) {
                    positionToInner(c, start, end);

                    cur = new FragSaveCur(start, end, synthName);
                } else {
                    start.moveToCur(c);
                    end.moveToCur(c);
                    end.skip();

                    cur = new FragSaveCur(start, end, null);
                }

                break;
            }
        }

        if (cur == null) {
            assert k < 0 || k == ATTR || k == COMMENT || k == PROCINST || k == TEXT;

            if (k < 0) {
                // Save out ""
                start.moveToCur(c);
                end.moveToCur(c);
            } else if (k == TEXT) {
                start.moveToCur(c);
                end.moveToCur(c);
                end.next();
            } else if (saveInner) {
                start.moveToCur(c);
                start.next();

                end.moveToCur(c);
                end.toEnd();
            } else if (k == ATTR) {
                start.moveToCur(c);
                end.moveToCur(c);
            } else {
                start.moveToCur(c);
                end.moveToCur(c);
                end.skip();
            }

            cur = new FragSaveCur(start, end, fragName);
        }

        String filterPI = options.getSaveFilterProcinst();

        if (filterPI != null) {
            cur = new FilterPiSaveCur(cur, filterPI);
        }

        if (options.isSavePrettyPrint()) {
            cur = new PrettySaveCur(cur, options);
        }

        start.release();
        end.release();

        return cur;
    }

    private static void positionToInner(Cur c, Cur start, Cur end) {
        assert c.isContainer();

        start.moveToCur(c);

        if (!start.toFirstAttr()) {
            start.next();
        }

        end.moveToCur(c);
        end.toEnd();
    }

    /**
     * Test if a character is valid in xml character content. See
     * http://www.w3.org/TR/REC-xml#NT-Char
     */
    static boolean isBadChar(char ch) {
        return !(
            Character.isHighSurrogate(ch) ||
            Character.isLowSurrogate(ch) ||
            (ch >= 0x20 && ch <= 0xD7FF) ||
            (ch >= 0xE000 && ch <= 0xFFFD) ||
            // TODO: ch >= 0x10000 && ch <= 0x10FFFF is always false for a char, use codepoints/ints
            // (ch >= 0x10000 && ch <= 0x10FFFF) ||
            (ch == 0x9) || (ch == 0xA) || (ch == 0xD)
        );
    }

    protected boolean saveNamespacesFirst() {
        return _saveNamespacesFirst;
    }

    protected final boolean process() {
        assert _locale.entered();

        if (_cur == null) {
            return false;
        }

        if (_version != _locale.version()) {
            throw new ConcurrentModificationException("Document changed during save");
        }

        switch (_cur.kind()) {
            case ROOT: {
                processRoot();
                break;
            }
            case ELEM: {
                processElement();
                break;
            }
            case -ELEM: {
                processFinish();
                break;
            }
            case TEXT: {
                emitText(_cur);
                break;
            }

            case COMMENT: {
                emitComment(_cur);
                _cur.toEnd();
                break;
            }
            case PROCINST: {
                emitProcinst(_cur);
                _cur.toEnd();
                break;
            }

            case -ROOT: {
                emitEndDoc(_cur);
                _cur.release();
                _cur = null;

                return true;
            }

            default:
                throw new RuntimeException("Unexpected kind");
        }

        _cur.next();

        return true;
    }

    private void processFinish() {
        emitFinish(_cur);
        popMappings();
    }

    private void processRoot() {
        assert _cur.isRoot();

        XmlDocumentProperties props = _cur.getDocProps();
        String systemId = null;
        String docTypeName = null;
        if (props != null) {
            systemId = props.getDoctypeSystemId();
            docTypeName = props.getDoctypeName();
        }

        if (systemId != null || docTypeName != null) {
            if (docTypeName == null) {
                _cur.push();
                //noinspection StatementWithEmptyBody
                while (!_cur.isElem() && _cur.next()) {
                }
                if (_cur.isElem()) {
                    docTypeName = _cur.getName().getLocalPart();
                }
                _cur.pop();
            }

            String publicId = props.getDoctypePublicId();

            if (docTypeName != null) {
                QName rootElemName = _cur.getName();

                if (rootElemName == null) {
                    _cur.push();
                    while (!_cur.isFinish()) {
                        if (_cur.isElem()) {
                            rootElemName = _cur.getName();
                            break;
                        }
                        _cur.next();
                    }
                    _cur.pop();
                }

                if (rootElemName != null && docTypeName.equals(rootElemName.getLocalPart())) {
                    emitDocType(docTypeName, publicId, systemId);
                    return;
                }
            }
        }

        emitStartDoc(_cur);
    }

    private void processElement() {
        assert _cur.isElem() && _cur.getName() != null;

        QName name = _cur.getName();

        // Add a new entry to the frontier.  If this element has a name
        // which has no namespace, then we must make sure that pushing
        // the mappings causes the default namespace to be empty

        boolean ensureDefaultEmpty = name.getNamespaceURI().length() == 0;

        pushMappings(_cur, ensureDefaultEmpty);

        //
        // There are four things which use mappings:
        //
        //   1) The element name
        //   2) The element value (qname based)
        //   3) Attribute names
        //   4) The attribute values (qname based)
        //

        // 1) The element name (not for starts)

        ensureMapping(name.getNamespaceURI(), name.getPrefix(), !ensureDefaultEmpty, false);

        //
        //
        //

        _attrNames.clear();
        _attrValues.clear();

        _cur.push();

        attrs:
        for (boolean A = _cur.toFirstAttr(); A; A = _cur.toNextAttr()) {
            if (_cur.isNormalAttr()) {
                QName attrName = _cur.getName();

                _attrNames.add(attrName);

                for (int i = _attrNames.size() - 2; i >= 0; i--) {
                    if (_attrNames.get(i).equals(attrName)) {
                        _attrNames.remove(_attrNames.size() - 1);
                        continue attrs;
                    }
                }

                _attrValues.add(_cur.getAttrValue());

                ensureMapping(attrName.getNamespaceURI(), attrName.getPrefix(), false, true);
            }
        }

        _cur.pop();

        // If I am doing aggressive namespaces and we're emitting a
        // container which can contain content, add the namespaces
        // we've computed.  Basically, I'm making sure the pre-computed
        // namespaces are mapped on the first container which has a name.

        if (_preComputedNamespaces != null) {
            for (Map.Entry<String, String> entry : _preComputedNamespaces.entrySet()) {
                String uri = entry.getKey();
                String prefix = entry.getValue();
                boolean considerDefault = prefix.length() == 0 && !ensureDefaultEmpty;

                ensureMapping(uri, prefix, considerDefault, false);
            }

            // Set to null so we do this once at the top
            _preComputedNamespaces = null;
        }

        if (emitElement(_cur, _attrNames, _attrValues)) {
            popMappings();
            _cur.toEnd();
        }
    }

    //
    // Layout of namespace stack:
    //
    //    URI Undo
    //    URI Rename
    //    Prefix Undo
    //    Mapping
    //

    void iterateMappings() {
        _currentMapping = _namespaceStack.size();

        while (_currentMapping > 0 && _namespaceStack.get(_currentMapping - 1) != null) {
            _currentMapping -= 8;
        }
    }

    boolean hasMapping() {
        return _currentMapping < _namespaceStack.size();
    }

    void nextMapping() {
        _currentMapping += 8;
    }

    String mappingPrefix() {
        assert hasMapping();
        return _namespaceStack.get(_currentMapping + 6);
    }

    String mappingUri() {
        assert hasMapping();
        return _namespaceStack.get(_currentMapping + 7);
    }

    private void pushMappings(SaveCur c, boolean ensureDefaultEmpty) {
        assert c.isContainer();

        _namespaceStack.add(null);

        c.push();

        for (boolean A = c.toFirstAttr(); A; A = c.toNextAttr()) {
            if (c.isXmlns()) {
                addNewFrameMapping(c.getXmlnsPrefix(), c.getXmlnsUri(), ensureDefaultEmpty);
            }
        }

        c.pop();

        if (_ancestorNamespaces != null) {
            for (int i = 0; i < _ancestorNamespaces.size(); i += 2) {
                String prefix = _ancestorNamespaces.get(i);
                String uri = _ancestorNamespaces.get(i + 1);

                addNewFrameMapping(prefix, uri, ensureDefaultEmpty);
            }

            _ancestorNamespaces = null;
        }

        if (ensureDefaultEmpty) {
            String defaultUri = _prefixMap.get("");

            // I map the default to "" at the very beginning
            assert defaultUri != null;

            if (defaultUri.length() > 0) {
                addMapping("", "");
            }
        }
    }

    private void addNewFrameMapping(String prefix, String uri, boolean ensureDefaultEmpty) {
        // If the prefix maps to "", then don't include this mapping 'cause it's not well formed.
        // Also, if we want to make sure that the default namespace is always "", then check that
        // here as well.

        if ((prefix.length() == 0 || uri.length() > 0) &&
            (!ensureDefaultEmpty || prefix.length() > 0 || uri.length() == 0)) {
            // Make sure the prefix is not already mapped in this frame

            for (iterateMappings(); hasMapping(); nextMapping()) {
                if (mappingPrefix().equals(prefix)) {
                    return;
                }
            }

            // Also make sure that the prefix declaration is not redundant
            // This has the side-effect of making it impossible to set a
            // redundant prefix declaration, but seems that it's better
            // to just never issue a duplicate prefix declaration.
            if (uri.equals(getNamespaceForPrefix(prefix))) {
                return;
            }

            addMapping(prefix, uri);
        }
    }

    private void addMapping(String prefix, String uri) {
        assert uri != null;
        assert prefix != null;

        // If the prefix being mapped here is already mapped to a uri,
        // that uri will either go out of scope or be mapped to another
        // prefix.

        String renameUri = _prefixMap.get(prefix);
        String renamePrefix = null;

        if (renameUri != null) {
            // See if this prefix is already mapped to this uri.  If
            // so, then add to the stack, but there is nothing to rename

            if (renameUri.equals(uri)) {
                renameUri = null;
            } else {
                int i = _namespaceStack.size();

                while (i > 0) {
                    if (_namespaceStack.get(i - 1) == null) {
                        i--;
                        continue;
                    }

                    if (_namespaceStack.get(i - 7).equals(renameUri)) {
                        renamePrefix = _namespaceStack.get(i - 8);

                        if (renamePrefix == null || !renamePrefix.equals(prefix)) {
                            break;
                        }
                    }

                    i -= 8;
                }

                assert i > 0;
            }
        }

        _namespaceStack.add(_uriMap.get(uri));
        _namespaceStack.add(uri);

        if (renameUri != null) {
            _namespaceStack.add(_uriMap.get(renameUri));
            _namespaceStack.add(renameUri);
        } else {
            _namespaceStack.add(null);
            _namespaceStack.add(null);
        }

        _namespaceStack.add(prefix);
        _namespaceStack.add(_prefixMap.get(prefix));

        _namespaceStack.add(prefix);
        _namespaceStack.add(uri);

        _uriMap.put(uri, prefix);
        _prefixMap.put(prefix, uri);

        if (renameUri != null) {
            _uriMap.put(renameUri, renamePrefix);
        }
    }

    private void popMappings() {
        for (; ; ) {
            int i = _namespaceStack.size();

            if (i == 0) {
                break;
            }

            if (_namespaceStack.get(i - 1) == null) {
                _namespaceStack.remove(i - 1);
                break;
            }

            String oldUri = _namespaceStack.get(i - 7);
            String oldPrefix = _namespaceStack.get(i - 8);

            if (oldPrefix == null) {
                _uriMap.remove(oldUri);
            } else {
                _uriMap.put(oldUri, oldPrefix);
            }

            oldPrefix = _namespaceStack.get(i - 4);
            oldUri = _namespaceStack.get(i - 3);

            if (oldUri == null) {
                _prefixMap.remove(oldPrefix);
            } else {
                _prefixMap.put(oldPrefix, oldUri);
            }

            String uri = _namespaceStack.get(i - 5);

            if (uri != null) {
                _uriMap.put(uri, _namespaceStack.get(i - 6));
            }

            // Hahahahahaha -- :-(
            _namespaceStack.remove(i - 1);
            _namespaceStack.remove(i - 2);
            _namespaceStack.remove(i - 3);
            _namespaceStack.remove(i - 4);
            _namespaceStack.remove(i - 5);
            _namespaceStack.remove(i - 6);
            _namespaceStack.remove(i - 7);
            _namespaceStack.remove(i - 8);
        }
    }

    private void ensureMapping(
        String uri, String candidatePrefix,
        boolean considerCreatingDefault, boolean mustHavePrefix) {
        assert uri != null;

        // Can be called for no-namespaced things

        if (uri.length() == 0) {
            return;
        }

        String prefix = _uriMap.get(uri);

        if (prefix != null && (prefix.length() > 0 || !mustHavePrefix)) {
            return;
        }

        //
        // I try prefixes from a number of places, in order:
        //
        //  1) What was passed in
        //  2) The optional suggestions (for uri's)
        //  3) The default mapping is allowed
        //  4) ns#++
        //

        if (candidatePrefix != null && candidatePrefix.length() == 0) {
            candidatePrefix = null;
        }

        if (!tryPrefix(candidatePrefix)) {
            if (_suggestedPrefixes != null &&
                _suggestedPrefixes.containsKey(uri) &&
                tryPrefix(_suggestedPrefixes.get(uri))) {
                candidatePrefix = _suggestedPrefixes.get(uri);
            } else if (considerCreatingDefault && _useDefaultNamespace && tryPrefix("")) {
                candidatePrefix = "";
            } else {
                String basePrefix = QNameHelper.suggestPrefix(uri);
                candidatePrefix = basePrefix;

                for (int i = 1; ; i++) {
                    if (tryPrefix(candidatePrefix)) {
                        break;
                    }

                    candidatePrefix = basePrefix + i;
                }
            }
        }

        assert candidatePrefix != null;

        syntheticNamespace(candidatePrefix, uri, considerCreatingDefault);

        addMapping(candidatePrefix, uri);
    }

    protected final String getUriMapping(String uri) {
        assert _uriMap.containsKey(uri);
        return _uriMap.get(uri);
    }

    String getNonDefaultUriMapping(String uri) {
        String prefix = _uriMap.get(uri);

        if (prefix != null && prefix.length() > 0) {
            return prefix;
        }

        for (String s : _prefixMap.keySet()) {
            prefix = s;

            if (prefix.length() > 0 && _prefixMap.get(prefix).equals(uri)) {
                return prefix;
            }
        }

        assert false : "Could not find non-default mapping";

        return null;
    }

    private boolean tryPrefix(String prefix) {
        if (prefix == null || Locale.beginsWithXml(prefix)) {
            return false;
        }

        String existingUri = _prefixMap.get(prefix);

        // If the prefix is currently mapped, then try another prefix.  A
        // special case is that of trying to map the default prefix ("").
        // Here, there always exists a default mapping.  If this is the
        // mapping we found, then remap it anyways. I use != to compare
        // strings because I want to test for the specific initial default
        // uri I added when I initialized the saver.

        return existingUri == null || (prefix.length() <= 0 && Objects.equals(existingUri, _initialDefaultUri));
    }

    public final String getNamespaceForPrefix(String prefix) {
        assert !prefix.equals("xml") || _prefixMap.get(prefix).equals(Locale._xml1998Uri);

        return _prefixMap.get(prefix);
    }


    static final class SynthNamespaceSaver extends Saver {
        LinkedHashMap<String, String> _synthNamespaces = new LinkedHashMap<>();

        SynthNamespaceSaver(Cur c, XmlOptions options) {
            super(c, options);
        }

        protected void syntheticNamespace(
            String prefix, String uri, boolean considerCreatingDefault) {
            _synthNamespaces.put(uri, considerCreatingDefault ? "" : prefix);
        }

        protected boolean emitElement(
            SaveCur c, List<QName> attrNames, List<String> attrValues) {
            return false;
        }

        protected void emitFinish(SaveCur c) {
        }

        protected void emitText(SaveCur c) {
        }

        protected void emitComment(SaveCur c) {
        }

        protected void emitProcinst(SaveCur c) {
        }

        protected void emitDocType(String docTypeName, String publicId, String systemId) {
        }

        protected void emitStartDoc(SaveCur c) {
        }

        protected void emitEndDoc(SaveCur c) {
        }
    }

    //
    //
    //

    static final class TextSaver extends Saver {
        TextSaver(Cur c, XmlOptions options, String encoding) {
            super(c, options);

            boolean noSaveDecl = options != null && options.isSaveNoXmlDecl();

            if (options != null && options.getSaveCDataLengthThreshold() != null) {
                _cdataLengthThreshold = options.getSaveCDataLengthThreshold();
            }

            if (options != null && options.getSaveCDataEntityCountThreshold() != null) {
                _cdataEntityCountThreshold = options.getSaveCDataEntityCountThreshold();
            }

            if (options != null && options.isUseCDataBookmarks()) {
                _useCDataBookmarks = true;
            }

            if (options != null && options.isSavePrettyPrint()) {
                _isPrettyPrint = true;
            }

            _in = _out = 0;
            _free = 0;

            //noinspection ConstantConditions
            assert _buf == null ||
                   (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            if (encoding != null && !noSaveDecl) {
                XmlDocumentProperties props = Locale.getDocProps(c, false);

                String version = props == null ? null : props.getVersion();

                if (version == null) {
                    version = "1.0";
                }

                Boolean standalone = null;
                if (props != null && props.get(XmlDocumentProperties.STANDALONE) != null) {
                    standalone = props.getStandalone();
                }

                emit("<?xml version=\"");
                emit(version);
                emit("\" encoding=\"" + encoding + "\"");
                if (standalone != null) {
                    emit(" standalone=\"" + (standalone ? "yes" : "no") + "\"");
                }
                emit("?>" + _newLine);
            }
        }

        @Override
        protected boolean emitElement(SaveCur c, List<QName> attrNames, List<String> attrValues) {
            assert c.isElem();

            emit('<');
            emitName(c.getName(), false);

            if (saveNamespacesFirst()) {
                emitNamespacesHelper();
            }

            for (int i = 0; i < attrNames.size(); i++) {
                emitAttrHelper(attrNames.get(i), attrValues.get(i));
            }

            if (!saveNamespacesFirst()) {
                emitNamespacesHelper();
            }

            if (!c.hasChildren() && !c.hasText()) {
                emit('/', '>');
                return true;
            } else {
                emit('>');
                return false;
            }
        }

        protected void emitFinish(SaveCur c) {
            emit('<', '/');
            emitName(c.getName(), false);
            emit('>');
        }

        protected void emitXmlns(String prefix, String uri) {
            assert prefix != null;
            assert uri != null;

            emit("xmlns");

            if (prefix.length() > 0) {
                emit(':');
                emit(prefix);
            }

            emit('=', '\"');

            // TODO - must encode uri properly

            emit(uri);
            entitizeAttrValue(false);

            emit('"');
        }

        private void emitNamespacesHelper() {
            LinkedHashMap<String, String> nsMap = new LinkedHashMap<>();
            for (iterateMappings(); hasMapping(); nextMapping()) {
                String prefix = mappingPrefix();
                String uri = mappingUri();
                if (nsMap.containsKey(prefix)) {
                    //only overwrite the nsMap entry for the prefix if the stored entry has prefix="" and uri=""
                    if (prefix.length() == 0 && nsMap.get(prefix).length() == 0) {
                        nsMap.put(prefix, uri);
                    }
                } else {
                    nsMap.put(prefix, uri);
                }
            }
            for (Map.Entry<String, String> nsEntry : nsMap.entrySet()) {
                emit(' ');
                emitXmlns(nsEntry.getKey(), nsEntry.getValue());
            }
        }

        private void emitAttrHelper(QName attrName, String attrValue) {
            emit(' ');
            emitName(attrName, true);
            emit('=', '\"');
            emit(attrValue);
            entitizeAttrValue(true);
            emit('"');
        }

        protected void emitText(SaveCur c) {
            assert c.isText();

            // c.isTextCData() is expensive do it only if useCDataBookmarks option is enabled
            boolean forceCData = _useCDataBookmarks && c.isTextCData();

            emit(c);

            entitizeContent(forceCData);
        }

        protected void emitComment(SaveCur c) {
            assert c.isComment();

            emit("<!--");

            c.push();
            c.next();

            emit(c);

            c.pop();

            entitizeComment();
            emit("-->");
        }

        protected void emitProcinst(SaveCur c) {
            assert c.isProcinst();

            emit("<?");

            // TODO - encoding issues here?
            emit(c.getName().getLocalPart());

            c.push();

            c.next();

            if (c.isText()) {
                emit(" ");
                emit(c);
                entitizeProcinst();
            }

            c.pop();

            emit("?>");
        }

        private void emitLiteral(String literal) {
            // TODO: systemId production http://www.w3.org/TR/REC-xml/#NT-SystemLiteral
            // TODO: publicId production http://www.w3.org/TR/REC-xml/#NT-PubidLiteral
            if (!literal.contains("\"")) {
                emit('\"');
                emit(literal);
                emit('\"');
            } else {
                emit('\'');
                emit(literal);
                emit('\'');
            }
        }

        protected void emitDocType(String docTypeName, String publicId, String systemId) {
            assert docTypeName != null;

            emit("<!DOCTYPE ");
            emit(docTypeName);

            if (publicId == null && systemId != null) {
                emit(" SYSTEM ");
                emitLiteral(systemId);
            } else if (publicId != null) {
                emit(" PUBLIC ");
                emitLiteral(publicId);
                emit(" ");
                emitLiteral(systemId);
            }

            emit(">");
            emit(_newLine);
        }

        protected void emitStartDoc(SaveCur c) {
        }

        protected void emitEndDoc(SaveCur c) {
        }

        //
        //
        //

        private void emitName(QName name, boolean needsPrefix) {
            assert name != null;

            String uri = name.getNamespaceURI();

            assert uri != null;

            if (uri.length() != 0) {
                String prefix = name.getPrefix();
                String mappedUri = getNamespaceForPrefix(prefix);

                if (mappedUri == null || !mappedUri.equals(uri)) {
                    prefix = getUriMapping(uri);
                }

                // Attrs need a prefix.  If I have not found one, then there must be a default
                // prefix obscuring the prefix needed for this attr.  Find it manually.

                // NOTE - Consider keeping the currently mapped default URI separate fromn the
                // _urpMap and _prefixMap.  This way, I would not have to look it up manually
                // here

                if (needsPrefix && prefix.length() == 0) {
                    prefix = getNonDefaultUriMapping(uri);
                }

                if (prefix.length() > 0) {
                    emit(prefix);
                    emit(':');
                }
            }

            assert name.getLocalPart().length() > 0;

            emit(name.getLocalPart());
        }

        private void emit(char ch) {
            assert _buf == null ||
                   (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            preEmit(1);

            _buf[_in] = ch;

            _in = (_in + 1) % _buf.length;

            assert (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;
        }

        private void emit(char ch1, char ch2) {
            if (preEmit(2)) {
                return;
            }

            _buf[_in] = ch1;
            _in = (_in + 1) % _buf.length;

            _buf[_in] = ch2;
            _in = (_in + 1) % _buf.length;

            assert (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;
        }

        private void emit(String s) {
            assert _buf == null ||
                   (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            int cch = s == null ? 0 : s.length();

            if (preEmit(cch) || s == null) {
                return;
            }

            int chunk;

            if (_in <= _out || cch < (chunk = _buf.length - _in)) {
                s.getChars(0, cch, _buf, _in);
                _in += cch;
            } else {
                s.getChars(0, chunk, _buf, _in);
                s.getChars(chunk, cch, _buf, 0);
                _in = (_in + cch) % _buf.length;
            }

            assert (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;
        }

        private void emit(SaveCur c) {
            if (c.isText()) {
                Object src = c.getChars();
                int cch = c._cchSrc;

                if (preEmit(cch)) {
                    return;
                }

                int chunk;

                if (_in <= _out || cch < (chunk = _buf.length - _in)) {
                    CharUtil.getChars(_buf, _in, src, c._offSrc, cch);
                    _in += cch;
                } else {
                    CharUtil.getChars(_buf, _in, src, c._offSrc, chunk);
                    CharUtil.getChars(_buf, 0, src, c._offSrc + chunk, cch - chunk);
                    _in = (_in + cch) % _buf.length;
                }
            } else {
                preEmit(0);
            }
        }

        private boolean preEmit(int cch) {
            assert cch >= 0;
            assert _buf == null ||
                   (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            _lastEmitCch = cch;

            if (cch == 0) {
                return true;
            }

            if (_free <= cch) {
                resize(cch, -1);
            }

            assert cch <= _free;

            int used = getAvailable();

            // if we are about to emit and there is noting in the buffer, reset
            // the buffer to be at the beginning so as to not grow it anymore
            // than needed.

            if (used == 0) {
                assert _in == _out;
                assert _buf == null || _free == _buf.length;
                _in = _out = 0;
            }

            _lastEmitIn = _in;

            _free -= cch;

            assert _buf == null || _free == (_in >= _out ? _buf.length - (_in - _out) : _out - _in) - cch : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;
            assert _buf == null ||
                   (_out < _in && _free == _buf.length - (_in - _out) - cch) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in - cch) ||                  // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length - cch) ||                 // no data, all buffer free
                   (_out == _in && _free == 0)                                    // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            return false;
        }

        private void entitizeContent(boolean forceCData) {
            assert _free >= 0;

            if (_lastEmitCch == 0) {
                return;
            }

            int i = _lastEmitIn;
            final int n = _buf.length;

            boolean hasCharToBeReplaced = false;

            int count = 0;
            char prevChar = 0;
            char prevPrevChar = 0;
            for (int cch = _lastEmitCch; cch > 0; cch--) {
                char ch = _buf[i];

                if (ch == '<' || ch == '&') {
                    count++;
                } else if (prevPrevChar == ']' && prevChar == ']' && ch == '>') {
                    hasCharToBeReplaced = true;
                } else if (isBadChar(ch) || isEscapedChar(ch) || (!_isPrettyPrint && ch == '\r')) {
                    hasCharToBeReplaced = true;
                }

                if (++i == n) {
                    i = 0;
                }

                prevPrevChar = prevChar;
                prevChar = ch;
            }

            if (!forceCData && count == 0 && !hasCharToBeReplaced && count < _cdataEntityCountThreshold) {
                return;
            }

            i = _lastEmitIn;

            //
            // Heuristic for knowing when to save out stuff as a CDATA.
            //
            if (forceCData || (_lastEmitCch > _cdataLengthThreshold && count > _cdataEntityCountThreshold)) {
                boolean lastWasBracket = _buf[i] == ']';

                i = replace(i, "<![CDATA[" + _buf[i]);

                boolean secondToLastWasBracket = lastWasBracket;

                lastWasBracket = _buf[i] == ']';

                if (++i == _buf.length) {
                    i = 0;
                }

                for (int cch = _lastEmitCch - 2; cch > 0; cch--) {
                    char ch = _buf[i];

                    if (ch == '>' && secondToLastWasBracket && lastWasBracket) {
                        i = replace(i, "]]>><![CDATA[");
                    } else if (isBadChar(ch)) {
                        i = replace(i, "?");
                    } else {
                        i++;
                    }

                    secondToLastWasBracket = lastWasBracket;
                    lastWasBracket = ch == ']';

                    if (i == _buf.length) {
                        i = 0;
                    }
                }

                emit("]]>");
            } else {
                char ch = 0, ch_1 = 0, ch_2;
                for (int cch = _lastEmitCch; cch > 0; cch--) {
                    ch_2 = ch_1;
                    ch_1 = ch;
                    ch = _buf[i];

                    if (ch == '<') {
                        i = replace(i, "&lt;");
                    } else if (ch == '&') {
                        i = replace(i, "&amp;");
                    } else if (ch == '>' && ch_1 == ']' && ch_2 == ']') {
                        i = replace(i, "&gt;");
                    } else if (isBadChar(ch)) {
                        i = replace(i, "?");
                    } else if (!_isPrettyPrint && ch == '\r') {
                        i = replace(i, "&#13;");
                    } else if (isEscapedChar(ch)) {
                        i = replace(i, _replaceChar.getEscapedString(ch));
                    } else {
                        i++;
                    }

                    if (i == _buf.length) {
                        i = 0;
                    }
                }
            }
        }

        private void entitizeAttrValue(boolean replaceEscapedChar) {
            if (_lastEmitCch == 0) {
                return;
            }

            int i = _lastEmitIn;

            for (int cch = _lastEmitCch; cch > 0; cch--) {
                char ch = _buf[i];

                if (ch == '<') {
                    i = replace(i, "&lt;");
                } else if (ch == '&') {
                    i = replace(i, "&amp;");
                } else if (ch == '"') {
                    i = replace(i, "&quot;");
                } else if (isEscapedChar(ch)) {
                    if (replaceEscapedChar) {
                        i = replace(i, _replaceChar.getEscapedString(ch));
                    }
                } else {
                    i++;
                }

                if (i == _buf.length) {
                    i = 0;
                }
            }
        }

        private void entitizeComment() {
            if (_lastEmitCch == 0) {
                return;
            }

            int i = _lastEmitIn;

            boolean lastWasDash = false;

            for (int cch = _lastEmitCch; cch > 0; cch--) {
                char ch = _buf[i];

                if (isBadChar(ch)) {
                    i = replace(i, "?");
                } else if (ch == '-') {
                    if (lastWasDash) {
                        // Replace "--" with "- " to make well formed
                        i = replace(i, " ");
                        lastWasDash = false;
                    } else {
                        lastWasDash = true;
                        i++;
                    }
                } else {
                    lastWasDash = false;
                    i++;
                }

                if (i == _buf.length) {
                    i = 0;
                }
            }

            // Because I have only replaced chars with single chars,
            // _lastEmitIn will still be ok

            int offset = (_lastEmitIn + _lastEmitCch - 1) % _buf.length;
            if (_buf[offset] == '-') {
                replace(offset, " ");
            }
        }

        private void entitizeProcinst() {
            if (_lastEmitCch == 0) {
                return;
            }

            int i = _lastEmitIn;

            boolean lastWasQuestion = false;

            for (int cch = _lastEmitCch; cch > 0; cch--) {
                char ch = _buf[i];

                if (isBadChar(ch)) {
                    i = replace(i, "?");
                }

                if (ch == '>') {
                    // TODO - Had to convert to a space here ... imples not well formed XML
                    if (lastWasQuestion) {
                        i = replace(i, " ");
                    } else {
                        i++;
                    }

                    lastWasQuestion = false;
                } else {
                    lastWasQuestion = ch == '?';
                    i++;
                }

                if (i == _buf.length) {
                    i = 0;
                }
            }
        }

        /**
         * Test if a character is to be replaced with an escaped value
         */
        private boolean isEscapedChar(char ch) {
            return (null != _replaceChar && _replaceChar.containsChar(ch));
        }

        private int replace(int i, String replacement) {
            assert replacement.length() > 0;

            int dCch = replacement.length() - 1;

            if (dCch == 0) {
                _buf[i] = replacement.charAt(0);
                return i + 1;
            }

            assert _free >= 0;

            if (dCch > _free) {
                i = resize(dCch, i);
            }

            assert _free >= 0;

            assert _free >= dCch;
            assert getAvailable() > 0;

            int charsToCopy = dCch + 1;

            if (_out > _in && i >= _out) {
                System.arraycopy(_buf, _out, _buf, _out - dCch, i - _out);
                _out -= dCch;
                i -= dCch;
            } else {
                assert i < _in;
                int availableEndChunk = _buf.length - _in;
                if (dCch <= availableEndChunk) {
                    System.arraycopy(_buf, i, _buf, i + dCch, _in - i);
                    _in = (_in + dCch) % _buf.length;
                } else if (dCch <= availableEndChunk + _in - i - 1) {
                    int numToCopyToStart = dCch - availableEndChunk;
                    System.arraycopy(_buf, _in - numToCopyToStart, _buf, 0, numToCopyToStart);
                    System.arraycopy(_buf, i + 1, _buf, i + 1 + dCch, _in - i - 1 - numToCopyToStart);

                    _in = numToCopyToStart;
                } else {
                    int numToCopyToStart = _in - i - 1;
                    charsToCopy = availableEndChunk + _in - i;

                    System.arraycopy(_buf, _in - numToCopyToStart, _buf, dCch - charsToCopy + 1, numToCopyToStart);
                    replacement.getChars(charsToCopy, dCch + 1, _buf, 0);

                    _in = numToCopyToStart + dCch - charsToCopy + 1;
                }
            }

            replacement.getChars(0, charsToCopy, _buf, i);

            _free -= dCch;

            assert _free >= 0;
            assert (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            return (i + dCch + 1) % _buf.length;
        }
        //
        //
        //

        private int ensure(int cch) {
            // Even if we're asked to ensure nothing, still try to ensure
            // atleast one character so we can determine if we're at the
            // end of the stream.

            if (cch <= 0) {
                cch = 1;
            }

            int available = getAvailable();

            for (; available < cch; available = getAvailable()) {
                if (!process()) {
                    break;
                }
            }

            assert available == getAvailable();

//            if (available == 0)
//                return 0;

            return available;
        }

        int getAvailable() {
            return _buf == null ? 0 : _buf.length - _free;
        }

        private int resize(int cch, int i) {
            assert _free >= 0;
            assert cch > 0;
            assert cch >= _free;
            assert _buf == null ||
                   (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            int newLen = _buf == null ? _initialBufSize : _buf.length * 2;
            int used = getAvailable();

            while (newLen - used < cch) {
                newLen *= 2;
            }

            char[] newBuf = new char[newLen];

            if (used > 0) {
                if (_in > _out) {
                    // Data in the middle (between _in and _out, i must be between those)
                    assert i == -1 || (i >= _out && i < _in);
                    System.arraycopy(_buf, _out, newBuf, 0, used);
                    i -= _out;
                } else {
                    // Data is on the edges: Oldest part from _in to end,
                    // newest part from 0 to _out.
                    // it must be in either of those regions
                    assert i == -1 || (i >= _out || i < _in);
                    int oldestSize = used - _in;
                    System.arraycopy( _buf, _out, newBuf, 0, oldestSize );
                    System.arraycopy( _buf, 0, newBuf, oldestSize, _in );
                    // newBuf now contains oldest data now at [0,oldestSize) and newest at [oldestSize,oldestSize+_in)
                    // Where was i? If past _out, i was in the oldest part, so adjust back by start of the oldest part
                    // Or: i was in the newest part, and should now be adjusted forward to where the newest part is now
                    i = (i >= _out) ? i - _out : i + oldestSize;
                }

                _out = 0;
                _in = used;
                _free += newBuf.length - _buf.length;
            } else {
                _free = newBuf.length;
                assert _in == 0 && _out == 0;
                assert i == -1;
            }

            _buf = newBuf;

            assert _free >= 0;
            //noinspection ConstantConditions
            assert _buf == null ||
                   (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            return i;
        }

        public int read() {
            if (ensure(1) == 0) {
                return -1;
            }

            assert getAvailable() > 0;

            int ch = _buf[_out];

            _out = (_out + 1) % _buf.length;
            _free++;

            assert (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            return ch;
        }

        public int read(char[] cbuf, int off, int len) {
            // Check for end of stream even if there is no way to return
            // characters because the Reader doc says to return -1 at end of
            // stream.

            int n;

            if ((n = ensure(len)) == 0) {
                return -1;
            }

            if (cbuf == null || len <= 0) {
                return 0;
            }

            if (n < len) {
                len = n;
            }

            if (_out < _in) {
                System.arraycopy(_buf, _out, cbuf, off, len);
            } else {
                int chunk = _buf.length - _out;

                if (chunk >= len) {
                    System.arraycopy(_buf, _out, cbuf, off, len);
                } else {
                    System.arraycopy(_buf, _out, cbuf, off, chunk);
                    System.arraycopy(_buf, 0, cbuf, off + chunk, len - chunk);
                }
            }

            _out = (_out + len) % _buf.length;
            _free += len;

            assert (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            assert _free >= 0;

            return len;
        }

        public int write(Writer writer, int cchMin) {
            while (getAvailable() < cchMin) {
                if (!process()) {
                    break;
                }
            }

            int charsAvailable = getAvailable();

            if (charsAvailable > 0) {
                // I don't want to deal with the circular cases

                assert _out == 0;
                assert _in >= _out : "_in:" + _in + " < _out:" + _out;
                assert _free == _buf.length - _in;

                try {
//System.out.println("-------------\nWriting in corverter: TextSaver.write():1703  " + charsAvailable + " chars\n" + new String(_buf, 0, charsAvailable));
                    writer.write(_buf, 0, charsAvailable);
                    writer.flush();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }

                _free += charsAvailable;

                assert _free >= 0;

                _in = 0;
            }
            assert _buf == null ||
                   (_out < _in && _free == _buf.length - (_in - _out)) || // data in the middle, free on the edges
                   (_out > _in && _free == _out - _in) ||                   // data on the edges, free in the middle
                   (_out == _in && _free == _buf.length) ||                  // no data, all buffer free
                   (_out == _in && _free == 0)                               // buffer full
                : "_buf.length:" + _buf.length + " _in:" + _in + " _out:" + _out + " _free:" + _free;

            return charsAvailable;
        }

        public String saveToString() {
            // We're gonna build a string.  Instead of using StringBuffer, may
            // as well use my buffer here.  Fill the whole sucker up and
            // create a String!

            //noinspection StatementWithEmptyBody
            while (process()) {
            }

            assert _out == 0;

            int available = getAvailable();

            return available == 0 ? "" : new String(_buf, _out, available);
        }

        //
        //
        //

        private static final int _initialBufSize = 4096;
        private int _cdataLengthThreshold = 32;
        private int _cdataEntityCountThreshold = 5;
        private boolean _useCDataBookmarks = false;
        private boolean _isPrettyPrint = false;

        private int _lastEmitIn;
        private int _lastEmitCch;

        private int _free;
        private int _in;
        private int _out;
        private char[] _buf;
        /*
        _buf is a circular buffer, useful data is before _in up to _out, there are 2 posible configurations:
        1: _in<=_out  |data|_in  empty  _out|data|
        2: _out<_in   |empty _out|data|_in  empty|
        _free is used to keep around the remaining empty space in the bufer so  assert _buf==null || _free == (_in>=_out ? _buf.length - (_in - _out) : _out - _in ) ;
         */
    }

    static final class OptimizedForSpeedSaver
        extends Saver {
        Writer _w;
        private final char[] _buf = new char[1024];


        static private class SaverIOException
            extends RuntimeException {
            SaverIOException(IOException e) {
                super(e);
            }
        }


        OptimizedForSpeedSaver(Cur cur, Writer writer) {
            super(cur, XmlOptions.maskNull(null));
            _w = writer;
        }

        static void save(Cur cur, Writer writer)
            throws IOException {
            try {
                Saver saver = new OptimizedForSpeedSaver(cur, writer);
                //noinspection StatementWithEmptyBody
                while (saver.process()) {
                }
            } catch (SaverIOException e) {
                throw (IOException) e.getCause();
            }
        }

        private void emit(String s) {
            try {
                _w.write(s);
            } catch (IOException e) {
                throw new SaverIOException(e);
            }
        }

        private void emit(char c) {
            try {
                _buf[0] = c;
                _w.write(_buf, 0, 1);
            } catch (IOException e) {
                throw new SaverIOException(e);
            }
        }

        private void emit(char c1, char c2) {
            try {
                _buf[0] = c1;
                _buf[1] = c2;
                _w.write(_buf, 0, 2);
            } catch (IOException e) {
                throw new SaverIOException(e);
            }
        }

        private void emit(char[] buf, int start, int len) {
            try {
                _w.write(buf, start, len);
            } catch (IOException e) {
                throw new SaverIOException(e);
            }
        }

        protected boolean emitElement(SaveCur c, List<QName> attrNames, List<String> attrValues) {
            assert c.isElem();

            emit('<');
            emitName(c.getName(), false);

            for (int i = 0; i < attrNames.size(); i++) {
                emitAttrHelper(attrNames.get(i), attrValues.get(i));
            }

            if (!saveNamespacesFirst()) {
                emitNamespacesHelper();
            }

            if (!c.hasChildren() && !c.hasText()) {
                emit('/', '>');
                return true;
            } else {
                emit('>');
                return false;
            }
        }

        protected void emitFinish(SaveCur c) {
            emit('<', '/');
            emitName(c.getName(), false);
            emit('>');
        }

        protected void emitXmlns(String prefix, String uri) {
            assert prefix != null;
            assert uri != null;

            emit("xmlns");

            if (prefix.length() > 0) {
                emit(':');
                emit(prefix);
            }

            emit('=', '\"');

            // TODO - must encode uri properly
            emitAttrValue(uri);

            emit('"');
        }

        private void emitNamespacesHelper() {
            for (iterateMappings(); hasMapping(); nextMapping()) {
                emit(' ');
                emitXmlns(mappingPrefix(), mappingUri());
            }
        }

        private void emitAttrHelper(QName attrName, String attrValue) {
            emit(' ');
            emitName(attrName, true);
            emit('=', '\"');
            emitAttrValue(attrValue);

            emit('"');
        }

        protected void emitComment(SaveCur c) {
            assert c.isComment();

            emit("<!--");

            c.push();
            c.next();

            emitCommentText(c);

            c.pop();

            emit("-->");
        }

        protected void emitProcinst(SaveCur c) {
            assert c.isProcinst();

            emit("<?");

            // TODO - encoding issues here?
            emit(c.getName().getLocalPart());

            c.push();

            c.next();

            if (c.isText()) {
                emit(' ');
                emitPiText(c);
            }

            c.pop();

            emit("?>");
        }

        protected void emitDocType(String docTypeName, String publicId, String systemId) {
            assert docTypeName != null;

            emit("<!DOCTYPE ");
            emit(docTypeName);

            if (publicId == null && systemId != null) {
                emit(" SYSTEM ");
                emitLiteral(systemId);
            } else if (publicId != null) {
                emit(" PUBLIC ");
                emitLiteral(publicId);
                emit(' ');
                emitLiteral(systemId);
            }

            emit('>');
            emit(_newLine);
        }

        protected void emitStartDoc(SaveCur c) {
        }

        protected void emitEndDoc(SaveCur c) {
        }

        //
        //
        //

        private void emitName(QName name, boolean needsPrefix) {
            assert name != null;

            String uri = name.getNamespaceURI();

            assert uri != null;

            if (uri.length() != 0) {
                String prefix = name.getPrefix();
                String mappedUri = getNamespaceForPrefix(prefix);

                if (mappedUri == null || !mappedUri.equals(uri)) {
                    prefix = getUriMapping(uri);
                }

                // Attrs need a prefix.  If I have not found one, then there must be a default
                // prefix obscuring the prefix needed for this attr.  Find it manually.

                // NOTE - Consider keeping the currently mapped default URI separate fromn the
                // _urpMap and _prefixMap.  This way, I would not have to look it up manually
                // here

                if (needsPrefix && prefix.length() == 0) {
                    prefix = getNonDefaultUriMapping(uri);
                }

                if (prefix.length() > 0) {
                    emit(prefix);
                    emit(':');
                }
            }

            assert name.getLocalPart().length() > 0;

            emit(name.getLocalPart());
        }

        private void emitAttrValue(CharSequence attVal) {
            int len = attVal.length();

            for (int i = 0; i < len; i++) {
                char ch = attVal.charAt(i);

                if (ch == '<') {
                    emit("&lt;");
                } else if (ch == '&') {
                    emit("&amp;");
                } else if (ch == '"') {
                    emit("&quot;");
                } else {
                    emit(ch);
                }
            }
        }

        private void emitLiteral(String literal) {
            // TODO: systemId production http://www.w3.org/TR/REC-xml/#NT-SystemLiteral
            // TODO: publicId production http://www.w3.org/TR/REC-xml/#NT-PubidLiteral
            if (!literal.contains("\"")) {
                emit('\"');
                emit(literal);
                emit('\"');
            } else {
                emit('\'');
                emit(literal);
                emit('\'');
            }
        }

        protected void emitText(SaveCur c) {
            assert c.isText();

            Object src = c.getChars();
            int cch = c._cchSrc;
            int off = c._offSrc;
            int index = 0;
            while (index < cch) {
                int indexLimit = Math.min(index + 512, cch);
                CharUtil.getChars(_buf, 0, src, off + index, indexLimit - index);
                entitizeAndWriteText(indexLimit - index);
                index = indexLimit;
            }
        }

        protected void emitPiText(SaveCur c) {
            assert c.isText();

            Object src = c.getChars();
            int cch = c._cchSrc;
            int off = c._offSrc;
            int index = 0;
            while (index < cch) {
                int indexLimit = index + 512 > cch ? cch : 512;
                CharUtil.getChars(_buf, 0, src, off + index, indexLimit);
                entitizeAndWritePIText(indexLimit - index);
                index = indexLimit;
            }
        }

        protected void emitCommentText(SaveCur c) {
            assert c.isText();

            Object src = c.getChars();
            int cch = c._cchSrc;
            int off = c._offSrc;
            int index = 0;
            while (index < cch) {
                int indexLimit = index + 512 > cch ? cch : 512;
                CharUtil.getChars(_buf, 0, src, off + index, indexLimit);
                entitizeAndWriteCommentText(indexLimit - index);
                index = indexLimit;
            }
        }

        private void entitizeAndWriteText(int bufLimit) {
            int index = 0;
            for (int i = 0; i < bufLimit; i++) {
                char c = _buf[i];
                switch (c) {
                    case '<':
                        emit(_buf, index, i - index);
                        emit("&lt;");
                        index = i + 1;
                        break;
                    case '&':
                        emit(_buf, index, i - index);
                        emit("&amp;");
                        index = i + 1;
                        break;
                }
            }
            emit(_buf, index, bufLimit - index);
        }

        private void entitizeAndWriteCommentText(int bufLimit) {
            boolean lastWasDash = false;

            for (int i = 0; i < bufLimit; i++) {
                char ch = _buf[i];

                if (isBadChar(ch)) {
                    _buf[i] = '?';
                } else if (ch == '-') {
                    if (lastWasDash) {
                        // Replace "--" with "- " to make well formed
                        _buf[i] = ' ';
                        lastWasDash = false;
                    } else {
                        lastWasDash = true;
                    }
                } else {
                    lastWasDash = false;
                }

                if (i == _buf.length) {
                    i = 0;
                }
            }

            if (_buf[bufLimit - 1] == '-') {
                _buf[bufLimit - 1] = ' ';
            }

            emit(_buf, 0, bufLimit);
        }

        private void entitizeAndWritePIText(int bufLimit) {
            boolean lastWasQuestion = false;

            for (int i = 0; i < bufLimit; i++) {
                char ch = _buf[i];

                if (isBadChar(ch)) {
                    _buf[i] = '?';
                    ch = '?';
                }

                if (ch == '>') {
                    // Had to convert to a space here ... imples not well formed XML
                    if (lastWasQuestion) {
                        _buf[i] = ' ';
                    }

                    lastWasQuestion = false;
                } else {
                    lastWasQuestion = ch == '?';
                }
            }
            emit(_buf, 0, bufLimit);
        }
    }

    private interface SyncWrapFun {
        int process() throws IOException;
    }

    private static int syncWrap(Locale l, SyncWrapFun fun) throws IOException {
        if (l.noSync()) {
            l.enter();
            try {
                return fun.process();
            } finally {
                l.exit();
            }
        } else {
            synchronized (l) {
                l.enter();
                try {
                    return fun.process();
                } finally {
                    l.exit();
                }
            }
        }
    }


    static final class TextReader extends Reader {
        TextReader(Cur c, XmlOptions options) {
            _textSaver = new TextSaver(c, options, null);
            _locale = c._locale;
            _closed = false;
        }

        public void close() {
            _closed = true;
        }

        public boolean ready() {
            return !_closed;
        }

        public int read() throws IOException {
            checkClosed();
            return syncWrap(_locale, _textSaver::read);
        }

        public int read(char[] cbuf) throws IOException {
            checkClosed();
            return syncWrap(_locale, () -> _textSaver.read(cbuf, 0, cbuf == null ? 0 : cbuf.length));
        }

        public int read(char[] cbuf, int off, int len) throws IOException {
            checkClosed();
            return syncWrap(_locale, () -> _textSaver.read(cbuf, off, len));
        }

        private void checkClosed() throws IOException {
            if (_closed) {
                throw new IOException("Reader has been closed");
            }
        }

        private final Locale _locale;
        private final TextSaver _textSaver;
        private boolean _closed;
    }

    static final class InputStreamSaver extends InputStream {
        InputStreamSaver(Cur c, XmlOptions options) {
            _locale = c._locale;

            _closed = false;

            assert _locale.entered();

            options = XmlOptions.maskNull(options);

            _outStreamImpl = new OutputStreamImpl();

            String encoding = null;

            XmlDocumentProperties props = Locale.getDocProps(c, false);

            if (props != null && props.getEncoding() != null) {
                encoding = EncodingMap.getIANA2JavaMapping(props.getEncoding());
            }

            String enc = options.getCharacterEncoding();
            if (enc != null) {
                encoding = enc;
            }

            if (encoding != null) {
                String ianaEncoding = EncodingMap.getJava2IANAMapping(encoding);

                if (ianaEncoding != null) {
                    encoding = ianaEncoding;
                }
            }

            if (encoding == null) {
                encoding = EncodingMap.getJava2IANAMapping("UTF8");
            }

            String javaEncoding = (encoding == null) ? null : EncodingMap.getIANA2JavaMapping(encoding);

            if (javaEncoding == null) {
                throw new IllegalStateException("Unknown encoding: " + encoding);
            }

            try {
                _converter = new OutputStreamWriter(_outStreamImpl, javaEncoding);
            } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
            }

            _textSaver = new TextSaver(c, options, encoding);
        }

        public void close() {
            _closed = true;
        }

        private void checkClosed() throws IOException {
            if (_closed) {
                throw new IOException("Stream closed");
            }
        }

        // Having the gateway here is kinda slow for the single character case.  It may be possible
        // to only enter the gate when there are no chars in the buffer.

        public int read() throws IOException {
            checkClosed();

            if (_locale.noSync()) {
                _locale.enter();
                try {
                    return _outStreamImpl.read();
                } finally {
                    _locale.exit();
                }
            } else {
                synchronized (_locale) {
                    _locale.enter();
                    try {
                        return _outStreamImpl.read();
                    } finally {
                        _locale.exit();
                    }
                }
            }
        }

        public int read(byte[] bbuf, int off, int len) throws IOException {
            checkClosed();

            if (bbuf == null) {
                throw new NullPointerException("buf to read into is null");
            }

            if (off < 0 || off > bbuf.length) {
                throw new IndexOutOfBoundsException("Offset is not within buf");
            }

            return syncWrap(_locale, () -> _outStreamImpl.read(bbuf, off, len));
        }

        private int ensure(int cbyte) {
            // Even if we're asked to ensure nothing, still try to ensure
            // atleast one byte so we can determine if we're at the
            // end of the stream.

            if (cbyte <= 0) {
                cbyte = 1;
            }

            int bytesAvailable = _outStreamImpl.getAvailable();

            for (; bytesAvailable < cbyte;
                 bytesAvailable = _outStreamImpl.getAvailable()) {
                if (_textSaver.write(_converter, 2048) < 2048) {
                    break;
                }
            }

            bytesAvailable = _outStreamImpl.getAvailable();

//            if (bytesAvailable == 0)
//                return 0;

            return bytesAvailable;
        }

        public int available() {
            try {
                return syncWrap(_locale, () -> ensure(1024));
            } catch (IOException e) {
                assert false : "ensure doesn't throw IOException and available() shouldn't throw either";
                throw new RuntimeException(e);
            }
        }

        private final class OutputStreamImpl extends OutputStream {
            int read() {
                if (InputStreamSaver.this.ensure(1) == 0) {
                    return -1;
                }

                assert getAvailable() > 0;

                int bite = _buf[_out];

                _out = (_out + 1) % _buf.length;
                _free++;

                return bite;
            }

            int read(byte[] bbuf, int off, int len) {
                // Check for end of stream even if there is no way to return
                // characters because the Reader doc says to return -1 at end of
                // stream.

                int n;

                if ((n = ensure(len)) == 0) {
                    return -1;
                }

                if (bbuf == null || len <= 0) {
                    return 0;
                }

                if (n < len) {
                    len = n;
                }

                if (_out < _in) {
                    System.arraycopy(_buf, _out, bbuf, off, len);
                } else {
                    int chunk = _buf.length - _out;

                    if (chunk >= len) {
                        System.arraycopy(_buf, _out, bbuf, off, len);
                    } else {
                        System.arraycopy(_buf, _out, bbuf, off, chunk);

                        System.arraycopy(
                            _buf, 0, bbuf, off + chunk, len - chunk);
                    }
                }
                _out = (_out + len) % _buf.length;
                _free += len;

//System.out.println("------------------------\nRead out of queue: Saver:2440 InputStreamSaver.read() bbuf   " + len + " bytes :\n" + new String(bbuf, off, len));
                return len;
            }

            int getAvailable() {
                return _buf == null ? 0 : _buf.length - _free;
            }

            public void write(int bite) {
                if (_free == 0) {
                    resize(1);
                }

                assert _free > 0;

                _buf[_in] = (byte) bite;

                _in = (_in + 1) % _buf.length;
                _free--;
            }

            public void write(byte[] buf, int off, int cbyte) {
                assert cbyte >= 0;
                if (cbyte == 0) {
                    return;
                }

                if (_free < cbyte) {
                    resize(cbyte);
                }

                if (_in == _out) {
                    assert getAvailable() == 0 && _buf != null && _free == _buf.length;
                    _in = _out = 0;
                }

                int chunk = _buf.length - _in;

                if (_in <= _out || cbyte < chunk) {
                    System.arraycopy(buf, off, _buf, _in, cbyte);
                    _in += cbyte;
                } else {
                    System.arraycopy(buf, off, _buf, _in, chunk);

                    System.arraycopy(
                        buf, off + chunk, _buf, 0, cbyte - chunk);

                    _in = (_in + cbyte) % _buf.length;
                }

                _free -= cbyte;
            }

            void resize(int cbyte) {
                assert cbyte > _free : cbyte + " !> " + _free;

                int newLen = _buf == null ? _initialBufSize : _buf.length * 2;
                int used = getAvailable();

                while (newLen - used < cbyte) {
                    newLen *= 2;
                }

                byte[] newBuf = new byte[newLen];

                if (used > 0) {
                    if (_in > _out) {
                        System.arraycopy(_buf, _out, newBuf, 0, used);
                    } else {
                        System.arraycopy(
                            _buf, _out, newBuf, 0, used - _in);

                        System.arraycopy(
                            _buf, 0, newBuf, used - _in, _in);
                    }

                    _out = 0;
                    _in = used;
                    _free += newBuf.length - _buf.length;
                } else {
                    _free = newBuf.length;
                    assert _in == _out;
                }

                _buf = newBuf;
            }

            private static final int _initialBufSize = 4096;

            private int _free;
            private int _in;
            private int _out;
            private byte[] _buf;
        }

        private final Locale _locale;
        private boolean _closed;
        private final OutputStreamImpl _outStreamImpl;
        private final TextSaver _textSaver;
        private final OutputStreamWriter _converter;
    }

    static final class SaxSaver extends Saver {
        SaxSaver(Cur c, XmlOptions options, ContentHandler ch, LexicalHandler lh)
            throws SAXException {
            super(c, options);

            _contentHandler = ch;
            _lexicalHandler = lh;

            _attributes = new AttributesImpl();
            _nsAsAttrs = !options.isSaveSaxNoNSDeclsInAttributes();

            _contentHandler.startDocument();

            try {
                //noinspection StatementWithEmptyBody
                while (process()) {
                }
            } catch (SaverSAXException e) {
                throw e._saxException;
            }

            _contentHandler.endDocument();
        }

        private static class SaverSAXException extends RuntimeException {
            SaverSAXException(SAXException e) {
                _saxException = e;
            }

            SAXException _saxException;
        }

        private String getPrefixedName(QName name) {
            String uri = name.getNamespaceURI();
            String local = name.getLocalPart();

            if (uri.length() == 0) {
                return local;
            }

            String prefix = getUriMapping(uri);

            if (prefix.length() == 0) {
                return local;
            }

            return prefix + ":" + local;
        }

        private void emitNamespacesHelper() {
            for (iterateMappings(); hasMapping(); nextMapping()) {
                String prefix = mappingPrefix();
                String uri = mappingUri();

                try {
                    _contentHandler.startPrefixMapping(prefix, uri);
                } catch (SAXException e) {
                    throw new SaverSAXException(e);
                }

                if (_nsAsAttrs) {
                    if (prefix == null || prefix.length() == 0) {
                        _attributes.addAttribute("http://www.w3.org/2000/xmlns/", "xmlns", "xmlns", "CDATA", uri);
                    } else {
                        _attributes.addAttribute("http://www.w3.org/2000/xmlns/", prefix, "xmlns:" + prefix, "CDATA", uri);
                    }
                }
            }
        }

        @Override
        protected boolean emitElement(SaveCur c, List<QName> attrNames, List<String> attrValues) {
            _attributes.clear();

            if (saveNamespacesFirst()) {
                emitNamespacesHelper();
            }

            for (int i = 0; i < attrNames.size(); i++) {
                QName name = attrNames.get(i);

                _attributes.addAttribute(
                    name.getNamespaceURI(), name.getLocalPart(), getPrefixedName(name),
                    "CDATA", attrValues.get(i));
            }

            if (!saveNamespacesFirst()) {
                emitNamespacesHelper();
            }

            QName elemName = c.getName();

            try {
                _contentHandler.startElement(
                    elemName.getNamespaceURI(), elemName.getLocalPart(),
                    getPrefixedName(elemName), _attributes);
            } catch (SAXException e) {
                throw new SaverSAXException(e);
            }

            return false;
        }

        protected void emitFinish(SaveCur c) {
            QName name = c.getName();

            try {
                _contentHandler.endElement(
                    name.getNamespaceURI(), name.getLocalPart(), getPrefixedName(name));

                for (iterateMappings(); hasMapping(); nextMapping()) {
                    _contentHandler.endPrefixMapping(mappingPrefix());
                }
            } catch (SAXException e) {
                throw new SaverSAXException(e);
            }
        }

        protected void emitText(SaveCur c) {
            assert c.isText();

            Object src = c.getChars();

            try {
                if (src instanceof char[]) {
                    // Pray the user does not modify the buffer ....
                    _contentHandler.characters((char[]) src, c._offSrc, c._cchSrc);
                } else {
                    if (_buf == null) {
                        _buf = new char[1024];
                    }

                    while (c._cchSrc > 0) {
                        int cch = java.lang.Math.min(_buf.length, c._cchSrc);

                        CharUtil.getChars(_buf, 0, src, c._offSrc, cch);

                        _contentHandler.characters(_buf, 0, cch);

                        c._offSrc += cch;
                        c._cchSrc -= cch;
                    }
                }
            } catch (SAXException e) {
                throw new SaverSAXException(e);
            }
        }

        protected void emitComment(SaveCur c) {
            if (_lexicalHandler != null) {
                c.push();

                c.next();

                try {
                    if (!c.isText()) {
                        _lexicalHandler.comment(null, 0, 0);
                    } else {
                        Object src = c.getChars();

                        if (src instanceof char[]) {
                            // Pray the user does not modify the buffer ....
                            _lexicalHandler.comment((char[]) src, c._offSrc, c._cchSrc);
                        } else {
                            if (_buf == null || _buf.length < c._cchSrc) {
                                _buf = new char[java.lang.Math.max(1024, c._cchSrc)];
                            }

                            CharUtil.getChars(_buf, 0, src, c._offSrc, c._cchSrc);

                            _lexicalHandler.comment(_buf, 0, c._cchSrc);
                        }
                    }
                } catch (SAXException e) {
                    throw new SaverSAXException(e);
                }

                c.pop();
            }
        }

        protected void emitProcinst(SaveCur c) {
            c.push();

            c.next();

            String value = CharUtil.getString(c.getChars(), c._offSrc, c._cchSrc);

            c.pop();

            try {
                _contentHandler.processingInstruction(c.getName().getLocalPart(), value);
            } catch (SAXException e) {
                throw new SaverSAXException(e);
            }
        }

        protected void emitDocType(String docTypeName, String publicId, String systemId) {
            if (_lexicalHandler != null) {
                try {
                    _lexicalHandler.startDTD(docTypeName, publicId, systemId);
                    _lexicalHandler.endDTD();
                } catch (SAXException e) {
                    throw new SaverSAXException(e);
                }
            }
        }

        protected void emitStartDoc(SaveCur c) {
        }

        protected void emitEndDoc(SaveCur c) {
        }

        private final ContentHandler _contentHandler;
        private final LexicalHandler _lexicalHandler;

        private final AttributesImpl _attributes;

        private char[] _buf;
        private final boolean _nsAsAttrs;
    }

    //
    //
    //

    static abstract class SaveCur {
        final boolean isRoot() {
            return kind() == ROOT;
        }

        final boolean isElem() {
            return kind() == ELEM;
        }

        final boolean isAttr() {
            return kind() == ATTR;
        }

        final boolean isText() {
            return kind() == TEXT;
        }

        final boolean isComment() {
            return kind() == COMMENT;
        }

        final boolean isProcinst() {
            return kind() == PROCINST;
        }

        final boolean isFinish() {
            return Cur.kindIsFinish(kind());
        }

        final boolean isContainer() {
            return Cur.kindIsContainer(kind());
        }

        final boolean isNormalAttr() {
            return kind() == ATTR && !isXmlns();
        }

        @SuppressWarnings("unused")
        final boolean skip() {
            toEnd();
            return next();
        }

        abstract void release();

        abstract int kind();

        abstract QName getName();

        abstract String getXmlnsPrefix();

        abstract String getXmlnsUri();

        abstract boolean isXmlns();

        abstract boolean hasChildren();

        abstract boolean hasText();

        abstract boolean isTextCData();

        abstract boolean toFirstAttr();

        abstract boolean toNextAttr();

        abstract String getAttrValue();

        abstract boolean next();

        abstract void toEnd();

        abstract void push();

        abstract void pop();

        abstract Object getChars();

        abstract List<String> getAncestorNamespaces();

        abstract XmlDocumentProperties getDocProps();

        int _offSrc;
        int _cchSrc;
    }

    // TODO - saving a fragment need to take namesapces from root and
    // reflect them on the document element

    private static final class DocSaveCur extends SaveCur {
        DocSaveCur(Cur c) {
            assert c.isRoot();
            _cur = c.weakCur(this);
        }

        void release() {
            _cur.release();
            _cur = null;
        }

        int kind() {
            return _cur.kind();
        }

        QName getName() {
            return _cur.getName();
        }

        String getXmlnsPrefix() {
            return _cur.getXmlnsPrefix();
        }

        String getXmlnsUri() {
            return _cur.getXmlnsUri();
        }

        boolean isXmlns() {
            return _cur.isXmlns();
        }

        boolean hasChildren() {
            return _cur.hasChildren();
        }

        boolean hasText() {
            return _cur.hasText();
        }

        boolean isTextCData() {
            return _cur.isTextCData();
        }

        boolean toFirstAttr() {
            return _cur.toFirstAttr();
        }

        boolean toNextAttr() {
            return _cur.toNextAttr();
        }

        String getAttrValue() {
            assert _cur.isAttr();
            return _cur.getValueAsString();
        }

        void toEnd() {
            _cur.toEnd();
        }

        boolean next() {
            return _cur.next();
        }

        void push() {
            _cur.push();
        }

        void pop() {
            _cur.pop();
        }

        List<String> getAncestorNamespaces() {
            return null;
        }

        Object getChars() {
            Object o = _cur.getChars(-1);

            _offSrc = _cur._offSrc;
            _cchSrc = _cur._cchSrc;

            return o;
        }

        XmlDocumentProperties getDocProps() {
            return Locale.getDocProps(_cur, false);
        }

        private Cur _cur;
    }

    private static abstract class FilterSaveCur extends SaveCur {
        FilterSaveCur(SaveCur c) {
            assert c.isRoot();
            _cur = c;
        }

        // Can filter anything by root and attributes and text
        protected abstract boolean filter();

        void release() {
            _cur.release();
            _cur = null;
        }

        int kind() {
            return _cur.kind();
        }

        QName getName() {
            return _cur.getName();
        }

        String getXmlnsPrefix() {
            return _cur.getXmlnsPrefix();
        }

        String getXmlnsUri() {
            return _cur.getXmlnsUri();
        }

        boolean isXmlns() {
            return _cur.isXmlns();
        }

        boolean hasChildren() {
            return _cur.hasChildren();
        }

        boolean hasText() {
            return _cur.hasText();
        }

        boolean isTextCData() {
            return _cur.isTextCData();
        }

        boolean toFirstAttr() {
            return _cur.toFirstAttr();
        }

        boolean toNextAttr() {
            return _cur.toNextAttr();
        }

        String getAttrValue() {
            return _cur.getAttrValue();
        }

        void toEnd() {
            _cur.toEnd();
        }

        boolean next() {
            if (!_cur.next()) {
                return false;
            }

            if (!filter()) {
                return true;
            }

            assert !isRoot() && !isText() && !isAttr();

            toEnd();

            return next();
        }

        void push() {
            _cur.push();
        }

        void pop() {
            _cur.pop();
        }

        List<String> getAncestorNamespaces() {
            return _cur.getAncestorNamespaces();
        }

        Object getChars() {
            Object o = _cur.getChars();

            _offSrc = _cur._offSrc;
            _cchSrc = _cur._cchSrc;

            return o;
        }

        XmlDocumentProperties getDocProps() {
            return _cur.getDocProps();
        }

        private SaveCur _cur;
    }

    private static final class FilterPiSaveCur extends FilterSaveCur {
        FilterPiSaveCur(SaveCur c, String target) {
            super(c);

            _piTarget = target;
        }

        protected boolean filter() {
            return kind() == PROCINST && getName().getLocalPart().equals(_piTarget);
        }

        private final String _piTarget;
    }

    private static final class FragSaveCur extends SaveCur {
        FragSaveCur(Cur start, Cur end, QName synthElem) {
            _saveAttr = start.isAttr() && start.isSamePos(end);

            _cur = start.weakCur(this);
            _end = end.weakCur(this);

            _elem = synthElem;

            _state = ROOT_START;

            _stateStack = new int[8];

            start.push();
            computeAncestorNamespaces(start);
            start.pop();
        }

        List<String> getAncestorNamespaces() {
            return _ancestorNamespaces;
        }

        private void computeAncestorNamespaces(Cur c) {
            _ancestorNamespaces = new ArrayList<>();

            while (c.toParentRaw()) {
                if (c.toFirstAttr()) {
                    do {
                        if (c.isXmlns()) {
                            String prefix = c.getXmlnsPrefix();
                            String uri = c.getXmlnsUri();

                            // Don't let xmlns:foo="" get used

                            if (uri.length() > 0 || prefix.length() == 0) {
                                _ancestorNamespaces.add(c.getXmlnsPrefix());
                                _ancestorNamespaces.add(c.getXmlnsUri());
                            }
                        }
                    }
                    while (c.toNextAttr());

                    c.toParent();
                }
            }
        }

        //
        //
        //

        void release() {
            _cur.release();
            _cur = null;

            _end.release();
            _end = null;
        }

        int kind() {
            switch (_state) {
                case ROOT_START:
                    return ROOT;
                case ELEM_START:
                    return ELEM;
                case ELEM_END:
                    return -ELEM;
                case ROOT_END:
                    return -ROOT;
            }

            assert _state == CUR;

            return _cur.kind();
        }

        QName getName() {
            switch (_state) {
                case ROOT_START:
                case ROOT_END:
                    return null;
                case ELEM_START:
                case ELEM_END:
                    return _elem;
            }

            assert _state == CUR;

            return _cur.getName();
        }

        String getXmlnsPrefix() {
            assert _state == CUR && _cur.isAttr();
            return _cur.getXmlnsPrefix();
        }

        String getXmlnsUri() {
            assert _state == CUR && _cur.isAttr();
            return _cur.getXmlnsUri();
        }

        boolean isXmlns() {
            assert _state == CUR && _cur.isAttr();
            return _cur.isXmlns();
        }

        boolean hasChildren() {
            boolean hasChildren = false;

            if (isContainer()) {   // is there a faster way to do this?
                push();
                next();

                if (!isText() && !isFinish()) {
                    hasChildren = true;
                }

                pop();
            }

            return hasChildren;
        }

        boolean hasText() {
            boolean hasText = false;

            if (isContainer()) {
                push();
                next();

                if (isText()) {
                    hasText = true;
                }

                pop();
            }

            return hasText;
        }

        boolean isTextCData() {
            return _cur.isTextCData();
        }

        Object getChars() {
            assert _state == CUR && _cur.isText();

            Object src = _cur.getChars(-1);

            _offSrc = _cur._offSrc;
            _cchSrc = _cur._cchSrc;

            return src;
        }

        boolean next() {
            switch (_state) {
                case ROOT_START: {
                    _state = _elem == null ? CUR : ELEM_START;
                    break;
                }

                case ELEM_START: {
                    if (_saveAttr) {
                        _state = ELEM_END;
                    } else {
                        if (_cur.isAttr()) {
                            _cur.toParent();
                            _cur.next();
                        }

                        if (_cur.isSamePos(_end)) {
                            _state = ELEM_END;
                        } else {
                            _state = CUR;
                        }
                    }

                    break;
                }

                case CUR: {
                    assert !_cur.isAttr();

                    _cur.next();

                    if (_cur.isSamePos(_end)) {
                        _state = _elem == null ? ROOT_END : ELEM_END;
                    }

                    break;
                }

                case ELEM_END: {
                    _state = ROOT_END;
                    break;
                }
                case ROOT_END:
                    return false;
            }

            return true;
        }

        void toEnd() {
            switch (_state) {
                case ROOT_START:
                    _state = ROOT_END;
                    return;
                case ELEM_START:
                    _state = ELEM_END;
                    return;
                case ROOT_END:
                case ELEM_END:
                    return;
            }

            assert _state == CUR && !_cur.isAttr() && !_cur.isText();

            _cur.toEnd();
        }

        boolean toFirstAttr() {
            switch (_state) {
                case ROOT_END:
                case ELEM_END:
                case ROOT_START:
                    return false;
                case CUR:
                    return _cur.toFirstAttr();
            }

            assert _state == ELEM_START;

            if (!_cur.isAttr()) {
                return false;
            }

            _state = CUR;

            return true;
        }

        boolean toNextAttr() {
            assert _state == CUR;
            return !_saveAttr && _cur.toNextAttr();
        }

        String getAttrValue() {
            assert _state == CUR && _cur.isAttr();
            return _cur.getValueAsString();
        }

        void push() {
            if (_stateStackSize == _stateStack.length) {
                int[] newStateStack = new int[_stateStackSize * 2];
                System.arraycopy(_stateStack, 0, newStateStack, 0, _stateStackSize);
                _stateStack = newStateStack;
            }

            _stateStack[_stateStackSize++] = _state;
            _cur.push();
        }

        void pop() {
            _cur.pop();
            _state = _stateStack[--_stateStackSize];
        }

        XmlDocumentProperties getDocProps() {
            return Locale.getDocProps(_cur, false);
        }

        //
        //
        //

        private Cur _cur;
        private Cur _end;

        private ArrayList<String> _ancestorNamespaces;

        private final QName _elem;

        private final boolean _saveAttr;

        private static final int ROOT_START = 1;
        private static final int ELEM_START = 2;
        private static final int ROOT_END = 3;
        private static final int ELEM_END = 4;
        private static final int CUR = 5;

        private int _state;

        private int[] _stateStack;
        private int _stateStackSize;
    }

    private static final class PrettySaveCur extends SaveCur {
        PrettySaveCur(SaveCur c, XmlOptions options) {
            _sb = new StringBuffer();
            _stack = new ArrayList<>();

            _cur = c;

            assert options != null;

            _prettyIndent = 2;

            if (options.getSavePrettyPrintIndent() != null) {
                _prettyIndent = options.getSavePrettyPrintIndent();
            }

            if (options.getSavePrettyPrintOffset() != null) {
                _prettyOffset = options.getSavePrettyPrintOffset();
            }

            _useCDataBookmarks = options.isUseCDataBookmarks();
        }

        List<String> getAncestorNamespaces() {
            return _cur.getAncestorNamespaces();
        }

        void release() {
            _cur.release();
        }

        int kind() {
            return _txt == null ? _cur.kind() : TEXT;
        }

        QName getName() {
            assert _txt == null;
            return _cur.getName();
        }

        String getXmlnsPrefix() {
            assert _txt == null;
            return _cur.getXmlnsPrefix();
        }

        String getXmlnsUri() {
            assert _txt == null;
            return _cur.getXmlnsUri();
        }

        boolean isXmlns() {
            return _txt == null && _cur.isXmlns();
        }

        boolean hasChildren() {
            return _txt == null && _cur.hasChildren();
        }

        boolean hasText() {
            return _txt == null && _cur.hasText();
        }

        // _cur.isTextCData() is expensive do it only if useCDataBookmarks option is enabled
        boolean isTextCData() {
            return _txt == null ? (_useCDataBookmarks && _cur.isTextCData())
                : _isTextCData;
        }

        boolean toFirstAttr() {
            assert _txt == null;
            return _cur.toFirstAttr();
        }

        boolean toNextAttr() {
            assert _txt == null;
            return _cur.toNextAttr();
        }

        String getAttrValue() {
            assert _txt == null;
            return _cur.getAttrValue();
        }

        void toEnd() {
            assert _txt == null;
            _cur.toEnd();

            if (_cur.kind() == -ELEM) {
                _depth--;
            }
        }

        boolean next() {
            int k;

            if (_txt != null) {
                assert _txt.length() > 0;
                assert !_cur.isText();
                _txt = null;
                _isTextCData = false;
                k = _cur.kind();
            } else {
                int prevKind = _cur.kind();

                if (!_cur.next()) {
                    return false;
                }

                _sb.delete(0, _sb.length());

                assert _txt == null;

                // place any text encountered in the buffer
                if (_cur.isText()) {
                    // _cur.isTextCData() is expensive do it only if useCDataBookmarks option is enabled
                    _isTextCData = _useCDataBookmarks && _cur.isTextCData();
                    CharUtil.getString(_sb, _cur.getChars(), _cur._offSrc, _cur._cchSrc);
                    _cur.next();
                    k = _cur.kind();
                    if (prevKind != ELEM || k != -ELEM) {
                        trim(_sb);
                    }
                }

                k = _cur.kind();

                // Check for non leaf, _prettyIndent < 0 means that the save is all on one line

                if (_prettyIndent >= 0 &&
                    prevKind != COMMENT && prevKind != PROCINST && (prevKind != ELEM || k != -ELEM)) {
                    if (_sb.length() > 0) {
                        _sb.insert(0, _newLine);
                        spaces(_sb, _newLine.length(), _prettyOffset + _prettyIndent * _depth);
                    }

                    if (k != -ROOT) {
                        if (prevKind != ROOT) {
                            _sb.append(_newLine);
                        }

                        int d = k < 0 ? _depth - 1 : _depth;
                        spaces(_sb, _sb.length(), _prettyOffset + _prettyIndent * d);
                    }
                }

                if (_sb.length() > 0) {
                    _txt = _sb.toString();
                    k = TEXT;
                }
            }

            if (k == ELEM) {
                _depth++;
            } else if (k == -ELEM) {
                _depth--;
            }

            return true;
        }

        void push() {
            _cur.push();
            _stack.add(_txt);
            _stack.add(_depth);
            _isTextCData = false;
        }

        void pop() {
            _cur.pop();
            _depth = (Integer) _stack.remove(_stack.size() - 1);
            _txt = (String) _stack.remove(_stack.size() - 1);
            _isTextCData = false;
        }

        Object getChars() {
            if (_txt != null) {
                _offSrc = 0;
                _cchSrc = _txt.length();
                return _txt;
            }

            Object o = _cur.getChars();

            _offSrc = _cur._offSrc;
            _cchSrc = _cur._cchSrc;

            return o;
        }

        XmlDocumentProperties getDocProps() {
            return _cur.getDocProps();
        }

        static void spaces(StringBuffer sb, int offset, int count) {
            while (count-- > 0) {
                sb.insert(offset, ' ');
            }
        }

        static void trim(StringBuffer sb) {
            int i;

            for (i = 0; i < sb.length(); i++) {
                if (!CharUtil.isWhiteSpace(sb.charAt(i))) {
                    break;
                }
            }

            sb.delete(0, i);

            for (i = sb.length(); i > 0; i--) {
                if (!CharUtil.isWhiteSpace(sb.charAt(i - 1))) {
                    break;
                }
            }

            sb.delete(i, sb.length());
        }

        private final SaveCur _cur;

        private int _prettyIndent;
        private int _prettyOffset;

        private String _txt;
        private final StringBuffer _sb;

        private int _depth;

        private final ArrayList<Object> _stack;
        private boolean _isTextCData = false;
        private final boolean _useCDataBookmarks;
    }
}
