/*
 *  Licensed to the Apache Software Foundation (ASF) under one
 *  or more contributor license agreements.  See the NOTICE file
 *  distributed with this work for additional information
 *  regarding copyright ownership.  The ASF licenses this file
 *  to you under the Apache License, Version 2.0 (the
 *  "License"); you may not use this file except in compliance
 *  with the License.  You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing,
 *  software distributed under the License is distributed on an
 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 *  KIND, either express or implied.  See the License for the
 *  specific language governing permissions and limitations
 *  under the License.
 */
package groovy.xml

import groovy.xml.streamingmarkupsupport.AbstractStreamingBuilder
import groovy.xml.streamingmarkupsupport.BaseMarkupBuilder
import groovy.xml.streamingmarkupsupport.StreamingMarkupWriter

/**
 * A builder class for creating XML markup.  This implementation uses a
 * {@link StreamingMarkupWriter} to handle output.
 * <p>
 * Example:
 * <pre>System.out << new StreamingMarkupBuilder().bind {
 *   root {
 *     a( a1:'one' ) {
 *       b { mkp.yield( '3 < 5' ) }
 *       c( a2:'two', 'blah' )
 *     }
 *   }
 * }</pre>
 * Will output the following String, without newlines or indentation:
 * <pre>&lt;root&gt;
 *   &lt;a a1='one'&gt;
 *     &lt;b&gt;3 &amp;lt; 5&lt;/b&gt;
 *     &lt;c a2='two'&gt;blah&lt;/c&gt;
 *   &lt;/a&gt;
 * &lt;/root&gt;</pre>
 * Notes:
 * <ul>
 *     <li>that <code>mkp</code> is a special namespace used to escape
 * away from the normal building mode of the builder and get access
 * to helper markup methods 'yield', 'pi', 'comment', 'out',
 * 'namespaces', 'xmlDeclaration' and 'yieldUnescaped'.
 * </li>
 *     <li>Note that tab, newline and carriage return characters are escaped within attributes, i.e. will become &#09;, &#10; and &#13; respectively</li>
 * </ul>
 */
@SuppressWarnings('Instanceof')
class StreamingMarkupBuilder extends AbstractStreamingBuilder {
    /** Whether generated XML prefers double quotes when quoting attributes and declarations. */
    boolean useDoubleQuotes = false
    /** Whether empty elements are rendered as explicit start/end pairs instead of empty-element tags. */
    boolean expandEmptyElements = false
    /**
     * Returns the quote character currently preferred for generated XML.
     *
     * @return the quote character string
     */
    def getQt() { useDoubleQuotes ? '"' : "'" }
    /** Stack of namespace declarations suspended while nested elements are rendered. */
    def pendingStack = []

    /**
     * Invoked by calling <code>mkp.comment</code>
     */
    def commentClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, out ->
        checkCommentText(body)
        out.unescaped() << '<!--'
        out.escaped() << body
        out.unescaped() << '-->'
    }

    /**
     * Invoked by calling <code>mkp.pi</code>
     */
    def piClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, out ->
        attrs.each {target, instruction ->
            checkProcessingInstruction(target)
            out.unescaped() << '<?'
            if (instruction instanceof Map) {
                out.unescaped() << target
                out.unescaped() << toMapStringClosure(instruction) { value ->
                    def valueStr = value.toString()
                    valueStr.contains('\'') || (useDoubleQuotes && !valueStr.contains('"'))
                }
            } else {
                checkProcessingInstruction(instruction)
                out.unescaped() << "$target $instruction"
            }
            out.unescaped() << '?>'
        }
    }

    /**
     * Invoked by calling <code>mkp.xmlDeclaration</code>
     */
    def declarationClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, out ->
        out.unescaped() << '<?xml version=' + qt + '1.0' + qt
        if (out.encodingKnown) out.escaped() << ' encoding=' + qt + out.encoding + qt
        out.unescaped() << '?>\n'
    }

    /**
     * Invoked by calling <code>mkp.yield</code>.  Used to render text to the
     * output stream.  Any XML reserved characters will be escaped to ensure
     * well-formedness.
     */
    def noopClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, out ->
        body.each {
            if (it instanceof Closure) {
                def body1 = it.clone()
                body1.delegate = doc
                body1(doc)
            } else if (it instanceof Buildable) {
                it.build(doc)
            } else {
                out.escaped() << it
            }
        }
    }

    /**
     * Invoked by calling <code>mkp.yieldUnescaped</code>.  Used to render
     * literal text or markup to the output stream.  No escaping is done on the
     * output.
     */
    def unescapedClosure = {doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, out ->
        out.unescaped() << body
    }

    /** Default element-emitting closure used for ordinary builder nodes. */
    def tagClosure = {tag, doc, pendingNamespaces, namespaces, namespaceSpecificTags, prefix, attrs, body, out ->
        boolean pendingIsDefaultNamespace = pendingNamespaces.containsKey(prefix) && !pendingNamespaces[prefix]
        if (prefix != '') {
            if (!(namespaces.containsKey(prefix) || pendingNamespaces.containsKey(prefix))) {
                throw new GroovyRuntimeException("Namespace prefix: ${prefix} is not bound to a URI")
            }
            if (prefix != ':' && !pendingIsDefaultNamespace) tag = prefix + ':' + tag
        }

        out = out.unescaped() << "<${tag}"

        attrs.each {key, value ->
            if (key.contains('$')) {
                def parts = key.tokenize('$')
                String localpart = parts[1].contains('}') ? parts[1].tokenize('}')[1] : parts[1]
                if (namespaces.containsKey(parts[0]) || pendingNamespaces.containsKey(parts[0])) {
                    key = parts[0] + ':' + localpart
                } else {
                    throw new GroovyRuntimeException("bad attribute namespace tag: ${parts[0]} in ${key}")
                }
            }

            out << " ${key}=" + qt
            out.writingAttribute = true
            "${value}".build(doc)
            out.writingAttribute = false
            out << qt
        }

        def hiddenNamespaces = [:]

        pendingNamespaces.each { key, value ->
            if (value) {
                hiddenNamespaces[key] = namespaces[key]
                namespaces[key] = value
                out << ((key == ':') ? ' xmlns=' + qt : " xmlns:${key}=" + qt)
                out.writingAttribute = true
                "${value}".build(doc)
                out.writingAttribute = false
                out << qt
            }
        }

        if (body == null && !expandEmptyElements) {
            out << '/>'
        } else {
            out << '>'

            pendingStack.push pendingNamespaces.clone()
            pendingNamespaces.clear()

            body.each {
                if (it instanceof Closure) {
                    def body1 = it.clone()
                    body1.delegate = doc
                    body1(doc)
                } else if (it instanceof Buildable) {
                    it.build(doc)
                } else {
                    out.escaped() << it
                }
            }

            pendingNamespaces.clear()
            pendingNamespaces.putAll pendingStack.pop()

            out << "</${tag}>"
        }

        hiddenNamespaces.each {key, value ->
            if (value == null) {
                namespaces.remove key
            } else {
                namespaces[key] = value
            }
        }
    }

    /** Backing namespace-aware builder reused for each {@link #bind(Closure)} call. */
    def builder = null

    /** Creates a streaming markup builder with the standard {@code mkp} helper tags installed. */
    StreamingMarkupBuilder() {
        specialTags.putAll([
            'yield'         : noopClosure,
            'yieldUnescaped': unescapedClosure,
            'xmlDeclaration': declarationClosure,
            'comment'       : commentClosure,
            'pi'            : piClosure
        ])

        def nsSpecificTags = [
            ':'                                             : [tagClosure, tagClosure, [:]], // the default namespace
            'http://www.w3.org/XML/1998/namespace'          : [tagClosure, tagClosure, [:]],
            'http://www.codehaus.org/Groovy/markup/keywords': [badTagClosure, tagClosure, specialTags]
        ]

        this.builder = new BaseMarkupBuilder(nsSpecificTags)
    }

    /** Optional encoding snapshot applied when bound markup is later written. */
    def encoding = null

    /**
     * Returns a {@link Writable} object, which may be used to render
     * the markup directly to a String, or send the output to a stream.
     * <p>
     * Examples:
     * <pre>
     * // get the markup as a string:
     * new StreamingMarkupBuilder().bind { div { out << 'hello world' } }.toString()
     *
     * // send the output directly to a file:
     * new StreamingMarkupBuilder().bind {
     *     div { out << 'hello world' }
     * }.writeTo( new File('myFile.xml').newWriter() )
     * </pre>
     *
     * @param closure closure describing the XML to generate
     * @return a {@link Writable} to render the markup
     */
    def bind(closure) {
        def boundClosure = this.builder.bind(closure)
        def enc = encoding  // take a snapshot of the encoding when the closure is bound to the builder

        def writable = { out ->
            out = new StreamingMarkupWriter(out, enc, useDoubleQuotes)
            boundClosure.trigger = out
            out.flush()
        }.asWritable()
        writable
    }

    /**
     * Convenience method for binding a single node.
     * The call <code>bindNode(node)</code> is equivalent to <code>bind{ out << node }</code>.
     * Returns a {@link Writable} object, which may be used to render
     * the markup directly to a String, or send the output to a stream.
     *
     * @see #bind(Closure)
     * @param node node or value to write through the builder
     * @return a {@link Writable} to render the markup
     */
    def bindNode(node) {
        bind { out << node }
    }
}
