/* Aalto XML processor
 *
 * Copyright (c) 2006- Tatu Saloranta, tatu.saloranta@iki.fi
 *
 * Licensed under the License specified in the file LICENSE which is
 * included with the source code.
 * You may not use this file except in compliance with the License.
 *
 * 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 com.fasterxml.aalto.out;

import java.util.*;

import javax.xml.namespace.QName;
import javax.xml.stream.*;

import org.codehaus.stax2.ri.typed.AsciiValueEncoder;

import com.fasterxml.aalto.impl.ErrorConsts;

/**
 * Concrete implementation of {@link StreamWriterBase}, which
 * implements the "namespace repairing" mode of operation.
 * This means that the writer ensures correctness and validity
 * of namespace bindings, as based on namespace URIs caller
 * passes, by adding necessary namespace declarations and using
 * prefixes as required to obtain expected results.
 */
public final class RepairingStreamWriter
    extends StreamWriterBase
{
    /*
    /////////////////////////////////////////////////////
    // Prefix generation settings
    /////////////////////////////////////////////////////
     */

    // // // Additional specific config flags base class doesn't have

    final String _cfgAutomaticNsPrefix;

    /*
    ////////////////////////////////////////////////////
    // Additional state
    ////////////////////////////////////////////////////
     */

    /**
     * Sequence number used for generating dynamic namespace prefixes.
     * Array used as a wrapper to allow for easy sharing of the sequence
     * number.
     */
    int[] _autoNsSeq = null;

    String _suggestedDefNs = null;

    /**
     * Map that contains URI-to-prefix entries that point out suggested
     * prefixes for URIs. These are populated by calls to
     * {@link #setPrefix}, and they are only used as hints for binding;
     * if there are conflicts, repairing writer can just use some other
     * prefix.
     */
    HashMap<String,String> _suggestedPrefixes = null;

    /*
    /////////////////////////////////////////////////////
    // Construction, init
    /////////////////////////////////////////////////////
     */

    public RepairingStreamWriter(WriterConfig cfg, XmlWriter writer,
                                 WNameTable symbols)
    {
        super(cfg, writer, symbols);
        _cfgAutomaticNsPrefix = cfg.getAutomaticNsPrefix();
    }

    /*
    /////////////////////////////////////////////////////
    // Implementations of abstract methods from base class,
    // Stax 1.0 methods
    /////////////////////////////////////////////////////
     */

    /**
     * With repairing writer, this is only taken as a suggestion as to how
     * the caller would prefer prefixes to be mapped.
     */
    @Override
    public void setDefaultNamespace(String uri)
        throws XMLStreamException
    {
        _suggestedDefNs = (uri == null || uri.length() == 0) ? null : uri;
    }

    @Override
    public void _setPrefix(String prefix, String uri)
    {
        // note: sub-class has verified that prefix != null, uri != null

        /* Ok; let's assume that passing an empty String as
         * the URI means that we don't want passed prefix to be preferred
         * for any URI (since there's no way to map a prefix to the default
         * namespace)
         */
        if (uri == null || uri.length() == 0) {
            if (_suggestedPrefixes != null) {
                for (Iterator<Map.Entry<String,String>> it =
                         _suggestedPrefixes.entrySet().iterator(); it.hasNext(); ) {
                    Map.Entry<String,String> en = it.next();
                    if (en.getValue().equals(prefix)) {
                        it.remove();
                    }
                }
            }
        } else {
            if (_suggestedPrefixes == null) {
                _suggestedPrefixes = new HashMap<String,String>(16);
            }
            _suggestedPrefixes.put(uri, prefix);
        }
    }

    //public void writeAttribute(String localName, String value)

    @Override
    public void writeAttribute(String nsURI, String localName, String value)
        throws XMLStreamException
    {
        if (!_stateStartElementOpen) {
            throwOutputError(ErrorConsts.WERR_ATTR_NO_ELEM);
        }
        // no URI? No prefix. Otherwise, need to find or bind:
        WName name = (nsURI == null || nsURI.length() == 0)
            ? _symbols.findSymbol(localName)
            : _generateAttrName(null, localName, nsURI);
        _writeAttribute(name, value);
    }

    @Override
    public void writeAttribute(String prefix, String nsURI,
                               String localName, String value)
        throws XMLStreamException
    {
        if (!_stateStartElementOpen) {
            throwOutputError(ErrorConsts.WERR_ATTR_NO_ELEM);
        }
        // no URI? No prefix. Otherwise, need to find or bind:
        WName name = (nsURI == null || nsURI.length() == 0)
            ? _symbols.findSymbol(localName)
            : _generateAttrName(prefix, localName, nsURI);
        _writeAttribute(name, value);
    }

    @Override
    public void writeDefaultNamespace(String nsURI)
        throws XMLStreamException
    {
        if (!_stateStartElementOpen) {
            throwOutputError(ErrorConsts.WERR_NS_NO_ELEM);
        }
         /* ... We have one complication though: if the current element
          * uses default namespace, can not change it (attributes don't
          * matter -- they never use the default namespace, but either don't
          * belong to a namespace, or belong to one using explicit prefix)
          */

        if (!_currElem.hasPrefix()) {
             _currElem.setDefaultNsURI(nsURI);
             _writeDefaultNamespace(nsURI);
         }
        _writeDefaultNamespace(nsURI);
    }

    //public void writeDTD(String dtd)

    //public void writeEmptyElement(String localName)

    @Override
    public void writeEmptyElement(String nsURI, String localName)
        throws XMLStreamException
    {
        _writeStartOrEmpty(null, localName, nsURI, true);
    }

    @Override
    public void writeEmptyElement(String prefix, String localName, String nsURI)
        throws XMLStreamException
    {
        _writeStartOrEmpty(prefix, localName, nsURI, true);
    }

    //public void writeEndElement()

    @Override
    public void writeNamespace(String prefix, String nsURI)
        throws XMLStreamException
    {
        if (prefix == null || prefix.length() == 0) {
            writeDefaultNamespace(nsURI);
            return;
        }
        if (!_stateStartElementOpen) {
            throwOutputError(ErrorConsts.WERR_NS_NO_ELEM);
        }
        /* Let's only add the declaration if the prefix
         * is as of yet unbound. If we have to re-bind things in future,
         * so be it -- for now, this should suffice (and if we have to
         * add re-binding, must verify that no attribute, nor element
         *  itself, is using overridden prefix)
         */
        if (_currElem.isPrefixUnbound(prefix, _rootNsContext)) {
            _currElem.addPrefix(prefix, nsURI);
            _writeNamespace(prefix, nsURI);
        }
    }

    //public void writeStartElement(String localName)

    @Override
    public void writeStartElement(String nsURI, String localName)
        throws XMLStreamException
    {
        _writeStartOrEmpty(null, localName, nsURI, false);
    }

    @Override
    public void writeStartElement(String prefix, String localName, String nsURI)
        throws XMLStreamException
    {
        _writeStartOrEmpty(prefix, localName, nsURI, false);
    }

    /*
    /////////////////////////////////////////////////////
    // Implementations of abstract methods from base class,
    // Stax2 Typed API Access (v3.0)
    /////////////////////////////////////////////////////
     */

    @Override
    public void writeTypedAttribute(String prefix, String nsURI, String localName,
            AsciiValueEncoder enc)
        throws XMLStreamException
    {
        if (!_stateStartElementOpen) {
            throwOutputError(ErrorConsts.WERR_ATTR_NO_ELEM);
        }
        WName name = (prefix == null || prefix.length() == 0)
            ? _symbols.findSymbol(localName)
            : _symbols.findSymbol(prefix, localName);
        _writeAttribute(name, enc);
    }

    @Override
    protected String _serializeQName(QName name) throws XMLStreamException
    {
        /* Gets bit more complicated: we need to ensure that given URI
         * is properly bound...
         */
        String uri = name.getNamespaceURI();
        String prefix = name.getPrefix();
        String local = name.getLocalPart();

        // Perhaps prefix is fine as is?
        if (_currElem.isPrefixBoundTo(prefix, uri, _rootNsContext)) {
            if (prefix == null || prefix.length() == 0) {
                return local;
            }
            return prefix + ":" + local;
        }

        /* Nope. Need to find or generate a new one... let's treat like an
         * attribute (i.e. get an explicit prefix, not use default ns).
         * Not optimal, need not be; not a hot spot method.
         */
        WName newName = _generateAttrName(prefix, local, uri);
        return newName.getPrefixedName();
    }

    /*
    /////////////////////////////////////////////////////
    // Internal methods
    /////////////////////////////////////////////////////
     */

    /**
     * @param uri Non-empty namespace URI that will be used for the
     *   attribute
     */
    protected WName _generateAttrName(String suggPrefix, String localName, String uri)
        throws XMLStreamException
    {
        if (suggPrefix != null && suggPrefix.length() > 0) { // prefer this prefix
            switch (_currElem.checkPrefixValidity(suggPrefix, uri, _rootNsContext)) {
            case UNBOUND: // ok, need to bind
                _writeNamespace(suggPrefix, uri);
                _currElem.addPrefix(suggPrefix, uri);
                // fall through:
            case OK: // fine as is
                return _symbols.findSymbol(suggPrefix, localName);
            case MISBOUND: // can't bind, need another prefix
                // fall through
            }
        }
        /* Had no suggested prefix, or one given didn't work. Either way,
         * need to find a match to URI, or generate new one.
         */
        String prefix = _currElem.getExplicitPrefix(uri, _rootNsContext);
        if (prefix == null) { // none found, generate
            if (_autoNsSeq == null) {
                _autoNsSeq = new int[1];
                _autoNsSeq[0] = 1;
            }
            prefix = _currElem.generatePrefix(_rootNsContext, _cfgAutomaticNsPrefix, _autoNsSeq);
            _writeNamespace(prefix, uri);
            _currElem.addPrefix(prefix, uri);
        }
        return _symbols.findSymbol(prefix, localName);
    }

    public void _writeStartOrEmpty(String prefix, String localName, String nsURI,
                                   boolean isEmpty)
        throws XMLStreamException
    {
        // In repairing mode, better ensure validity.

        // First: do we want the "no namespace"? Separate, distinct handling
        if (nsURI == null || nsURI.length() == 0) {
            // either way, can not have non-empty prefix
            _verifyStartElement(null, localName);
            // must output the start-tag-start to add xmlns decl (if needed)
            WName name = _symbols.findSymbol(localName);
            _writeStartTag(name, isEmpty, null);
            if (!_currElem.hasEmptyDefaultNs()) { // is bound, need to rebind...
                _writeDefaultNamespace(nsURI);
                // also: changes default ns of curr elem etc:
                _currElem.setDefaultNsURI("");
            }
            if (_validator != null) {
                _validator.validateElementStart(localName, "", "");
            }
            return;
        }

        // Otherwise a non-empty namespace. Do we have a suggested prefix?
        if (prefix == null) { // nope, anything goes
            prefix = _currElem.getPrefix(nsURI);
            // And we do have something bound already!
            if (prefix != null) {
                _writeStartAndVerify(prefix, localName, nsURI, isEmpty);
            } else {
                // nothing suitable bound, let's generate
                prefix = _generateElemPrefix(nsURI);
                if (_writeStartAndVerify(prefix, localName, nsURI, isEmpty)) { // default ns
                    _writeDefaultNamespace(nsURI);
                    _currElem.setDefaultNsURI(nsURI);
                } else { // non-default, explicit prefix
                    _writeNamespace(prefix, nsURI);
                    _currElem.addPrefix(prefix, nsURI);
                }
            }
        } else {
            boolean isDef = _writeStartAndVerify(prefix, localName, nsURI, isEmpty);
            // Is the prefix already properly bound?
            if (!_currElem.isPrefixBoundTo(prefix, nsURI, _rootNsContext)) { // yup
                // Nope, need to rebind
                if (isDef) {
                    _writeDefaultNamespace(nsURI);
                    _currElem.setDefaultNsURI(nsURI);
                } else { // explicit prefix
                    _writeNamespace(prefix, nsURI);
                    _currElem.addPrefix(prefix, nsURI);
                }
            }
        }

        // And after all that, validation?
        if (_validator != null) {
            _validator.validateElementStart(localName, "",
                                            ((prefix == null) ? "" : prefix));
        }
        return;
    }

    /**
     * @return True, if prefix indicates default namespace (is null or empty);
     *   false otherwise
     */
    private final boolean _writeStartAndVerify(String prefix, String localName, String nsURI,
                                               boolean isEmpty)
        throws XMLStreamException
    {
        if (prefix == null || prefix.length() == 0) { // default ns
            _verifyStartElement(null, localName);
            _writeStartTag(_symbols.findSymbol(localName), isEmpty, nsURI);
            return true;
        }
        // non-default, i.e. real prefix
        _verifyStartElement(prefix, localName);
        _writeStartTag(_symbols.findSymbol(prefix, localName), isEmpty, nsURI);
        return false;
    }

    /**
     * Method called if given URI is not yet bound, and no suggested prefix
     * is given (or one given can't be used). If so, methods is
     * to create a not-yet-bound-prefix for the namespace.
     */
    protected final String _generateElemPrefix(String uri)
        throws XMLStreamException
    {
        // First: is it the 'recommended' default ns?
        if (_suggestedDefNs != null && _suggestedDefNs.equals(uri)) {
            return null;
        }
        // If not, maybe one of other 'recommended' bindings has it?
        if (_suggestedPrefixes != null) {
            String prefix = _suggestedPrefixes.get(uri);
            if (prefix != null) {
                return prefix;
            }
        }
        // Otherwise, generate a new unbound prefix

        /* We have 2 choices here, essentially;
         *   could make elements always try to override the def
         *   ns... or can just generate new one. Let's do latter
         *   for now.
         */
        if (_autoNsSeq == null) {
            _autoNsSeq = new int[1];
            _autoNsSeq[0] = 1;
        }
        return _currElem.generatePrefix(_rootNsContext, _cfgAutomaticNsPrefix, _autoNsSeq);
    }
}
