
//          Copyright Ferdinand Majerech 2011.
// Distributed under the Boost Software License, Version 1.0.
//    (See accompanying file LICENSE_1_0.txt or copy at
//          http://www.boost.org/LICENSE_1_0.txt)

/**
 * Composes nodes from YAML events provided by parser.
 * Code based on PyYAML: http://www.pyyaml.org
 */
module dyaml.composer;

import core.memory;

import std.array;
import std.conv;
import std.exception;
import std.typecons;

import dyaml.anchor;
import dyaml.constructor;
import dyaml.event;
import dyaml.exception;
import dyaml.node;
import dyaml.parser;
import dyaml.resolver;


package:
/**
 * Exception thrown at composer errors.
 *
 * See_Also: MarkedYAMLException
 */
class ComposerException : MarkedYAMLException
{
    mixin MarkedExceptionCtors;
}

///Composes YAML documents from events provided by a Parser.
final class Composer
{
    private:
        ///Parser providing YAML events.
        Parser parser_;
        ///Resolver resolving tags (data types).
        Resolver resolver_;
        ///Constructor constructing YAML values.
        Constructor constructor_;
        ///Nodes associated with anchors. Used by YAML aliases.
        Node[Anchor] anchors_;

        ///Used to reduce allocations when creating pair arrays.
        ///
        ///We need one appender for each nesting level that involves
        ///a pair array, as the inner levels are processed as a
        ///part of the outer levels. Used as a stack.
        Appender!(Node.Pair[], Node.Pair)[] pairAppenders_;
        ///Used to reduce allocations when creating node arrays.
        ///
        ///We need one appender for each nesting level that involves
        ///a node array, as the inner levels are processed as a
        ///part of the outer levels. Used as a stack.
        Appender!(Node[], Node)[] nodeAppenders_;

    public:
        /**
         * Construct a composer.
         *
         * Params:  parser      = Parser to provide YAML events.
         *          resolver    = Resolver to resolve tags (data types).
         *          constructor = Constructor to construct nodes.
         */
        this(Parser parser, Resolver resolver, Constructor constructor) @safe
        {
            parser_ = parser;
            resolver_ = resolver;
            constructor_ = constructor;
        }

        ///Destroy the composer.
        pure @safe nothrow ~this()
        {
            parser_ = null;
            resolver_ = null;
            constructor_ = null;
            clear(anchors_);
            anchors_ = null;
        }

        /**
         * Determine if there are any nodes left.
         *
         * Must be called before loading as it handles the stream start event.
         */
        bool checkNode() @safe
        {
            //Drop the STREAM-START event.
            if(parser_.checkEvent(EventID.StreamStart))
            {
                parser_.getEvent();
            }

            //True if there are more documents available.
            return !parser_.checkEvent(EventID.StreamEnd);
        }

        ///Get a YAML document as a node (the root of the document).
        Node getNode() @safe
        {
            //Get the root node of the next document.
            assert(!parser_.checkEvent(EventID.StreamEnd), 
                   "Trying to get a node from Composer when there is no node to "
                   "get. use checkNode() to determine if there is a node.");

            return composeDocument();
        }

        ///Get single YAML document, throwing if there is more than one document.
        Node getSingleNode() @trusted
        {
            assert(!parser_.checkEvent(EventID.StreamEnd), 
                   "Trying to get a node from Composer when there is no node to "
                   "get. use checkNode() to determine if there is a node.");

            Node document = composeDocument();

            //Ensure that the stream contains no more documents.
            enforce(parser_.checkEvent(EventID.StreamEnd),
                    new ComposerException("Expected single document in the stream, "
                                          "but found another document.",
                                          parser_.getEvent().startMark));

            //Drop the STREAM-END event.
            parser_.getEvent();

            return document;
        }

    private:
        ///Ensure that appenders for specified nesting levels exist.
        ///
        ///Params:  pairAppenderLevel = Current level in the pair appender stack.
        ///         nodeAppenderLevel = Current level the node appender stack.
        void ensureAppendersExist(const uint pairAppenderLevel, const uint nodeAppenderLevel) 
            @trusted
        {
            while(pairAppenders_.length <= pairAppenderLevel)
            {
                pairAppenders_ ~= appender!(Node.Pair[])();
            }
            while(nodeAppenders_.length <= nodeAppenderLevel)
            {
                nodeAppenders_ ~= appender!(Node[])();
            }
        }

        ///Compose a YAML document and return its root node.
        Node composeDocument() @trusted
        {
            //Drop the DOCUMENT-START event.
            parser_.getEvent();

            //Compose the root node.
            Node node = composeNode(0, 0);

            //Drop the DOCUMENT-END event.
            parser_.getEvent();

            clear(anchors_);
            return node;
        }

        /// Compose a node.
        ///
        /// Params: pairAppenderLevel = Current level of the pair appender stack.
        ///         nodeAppenderLevel = Current level of the node appender stack.
        Node composeNode(const uint pairAppenderLevel, const uint nodeAppenderLevel) @system
        {
            if(parser_.checkEvent(EventID.Alias))
            {
                immutable event = parser_.getEvent();
                const anchor = event.anchor;
                enforce((anchor in anchors_) !is null,
                        new ComposerException("Found undefined alias: " ~ anchor.get,
                                              event.startMark));

                //If the node referenced by the anchor is uninitialized,
                //it's not finished, i.e. we're currently composing it
                //and trying to use it recursively here.
                enforce(anchors_[anchor] != Node(),
                        new ComposerException("Found recursive alias: " ~ anchor.get,
                                              event.startMark));

                return anchors_[anchor];
            }

            immutable event = parser_.peekEvent();
            const anchor = event.anchor;
            if(!anchor.isNull() && (anchor in anchors_) !is null)
            {
                throw new ComposerException("Found duplicate anchor: " ~ anchor.get, 
                                            event.startMark);
            }

            Node result;
            //Associate the anchor, if any, with an uninitialized node.
            //used to detect duplicate and recursive anchors.
            if(!anchor.isNull())
            {
                anchors_[anchor] = Node();
            }

            if(parser_.checkEvent(EventID.Scalar))
            {
                result = composeScalarNode();
            }
            else if(parser_.checkEvent(EventID.SequenceStart))
            {
                result = composeSequenceNode(pairAppenderLevel, nodeAppenderLevel);
            }
            else if(parser_.checkEvent(EventID.MappingStart))
            {
                result = composeMappingNode(pairAppenderLevel, nodeAppenderLevel);
            }
            else{assert(false, "This code should never be reached");}

            if(!anchor.isNull())
            {
                anchors_[anchor] = result;
            }
            return result;
        }

        ///Compose a scalar node.
        Node composeScalarNode() @system
        {
            immutable event = parser_.getEvent();
            const tag = resolver_.resolve(NodeID.Scalar, event.tag, event.value, 
                                          event.implicit);

            Node node = constructor_.node(event.startMark, event.endMark, tag, 
                                          event.value, event.scalarStyle);

            return node;
        }

        /// Compose a sequence node.
        ///
        /// Params: pairAppenderLevel = Current level of the pair appender stack.
        ///         nodeAppenderLevel = Current level of the node appender stack.
        Node composeSequenceNode(const uint pairAppenderLevel, const uint nodeAppenderLevel) 
            @system
        {
            ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
            auto nodeAppender = &(nodeAppenders_[nodeAppenderLevel]);

            immutable startEvent = parser_.getEvent();
            const tag = resolver_.resolve(NodeID.Sequence, startEvent.tag, null, 
                                          startEvent.implicit);

            while(!parser_.checkEvent(EventID.SequenceEnd))
            {
                nodeAppender.put(composeNode(pairAppenderLevel, nodeAppenderLevel + 1));
            }

            core.memory.GC.disable();
            scope(exit){core.memory.GC.enable();}
            Node node = constructor_.node(startEvent.startMark, parser_.getEvent().endMark, 
                                          tag, nodeAppender.data.dup, startEvent.collectionStyle);
            nodeAppender.clear();

            return node;
        }

        /**
         * Flatten a node, merging it with nodes referenced through YAMLMerge data type.
         *
         * Node must be a mapping or a sequence of mappings.
         *
         * Params:  root              = Node to flatten.
         *          startMark         = Start position of the node.
         *          endMark           = End position of the node.
         *          pairAppenderLevel = Current level of the pair appender stack.
         *          nodeAppenderLevel = Current level of the node appender stack.
         *
         * Returns: Flattened mapping as pairs.
         */
        Node.Pair[] flatten(ref Node root, const Mark startMark, const Mark endMark,
                            const uint pairAppenderLevel, const uint nodeAppenderLevel) @system
        {
            void error(Node node)
            {
                //this is Composer, but the code is related to Constructor.
                throw new ConstructorException("While constructing a mapping, "
                                               "expected a mapping or a list of "
                                               "mappings for merging, but found: " 
                                               ~ node.type.toString() ~
                                               " NOTE: line/column shows topmost parent "
                                               "to which the content is being merged",
                                               startMark, endMark);
            }

            ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
            auto pairAppender = &(pairAppenders_[pairAppenderLevel]);

            if(root.isMapping)
            {
                Node[] toMerge;
                foreach(ref Node key, ref Node value; root)
                {
                    if(key.isType!YAMLMerge){toMerge ~= value;}
                    else
                    {
                        auto temp = Node.Pair(key, value);
                        merge(*pairAppender, temp);
                    }
                }
                foreach(node; toMerge)
                {
                    merge(*pairAppender, flatten(node, startMark, endMark, 
                                                 pairAppenderLevel + 1, nodeAppenderLevel));
                }
            }
            //Must be a sequence of mappings.
            else if(root.isSequence) foreach(ref Node node; root)
            {
                if(!node.isType!(Node.Pair[])){error(node);}
                merge(*pairAppender, flatten(node, startMark, endMark, 
                                             pairAppenderLevel + 1, nodeAppenderLevel));
            }
            else
            {
                error(root);
            }

            core.memory.GC.disable();
            scope(exit){core.memory.GC.enable();}
            auto flattened = pairAppender.data.dup;
            pairAppender.clear();

            return flattened;
        }

        /// Compose a mapping node.
        ///
        /// Params: pairAppenderLevel = Current level of the pair appender stack.
        ///         nodeAppenderLevel = Current level of the node appender stack.
        Node composeMappingNode(const uint pairAppenderLevel, const uint nodeAppenderLevel)
            @system
        {
            ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
            immutable startEvent = parser_.getEvent();
            const tag = resolver_.resolve(NodeID.Mapping, startEvent.tag, null, 
                                          startEvent.implicit);
            auto pairAppender = &(pairAppenders_[pairAppenderLevel]);

            Tuple!(Node, Mark)[] toMerge;
            while(!parser_.checkEvent(EventID.MappingEnd))
            {
                auto pair = Node.Pair(composeNode(pairAppenderLevel + 1, nodeAppenderLevel), 
                                      composeNode(pairAppenderLevel + 1, nodeAppenderLevel));

                //Need to flatten and merge the node referred by YAMLMerge.
                if(pair.key.isType!YAMLMerge)
                {
                    toMerge ~= tuple(pair.value, cast(Mark)parser_.peekEvent().endMark);
                }
                //Not YAMLMerge, just add the pair.
                else
                {
                    merge(*pairAppender, pair);
                }
            }
            foreach(node; toMerge)
            {
                merge(*pairAppender, flatten(node[0], startEvent.startMark, node[1], 
                                             pairAppenderLevel + 1, nodeAppenderLevel));
            }

            core.memory.GC.disable();
            scope(exit){core.memory.GC.enable();}
            Node node = constructor_.node(startEvent.startMark, parser_.getEvent().endMark, 
                                          tag, pairAppender.data.dup, startEvent.collectionStyle);

            pairAppender.clear();
            return node;
        }
}
