//          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.algorithm;
import std.array;
import std.conv;
import std.exception;
import std.format;
import std.range;
import std.typecons;

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.
struct Composer
{
    private:
        ///Parser providing YAML events.
        Parser parser_;
        ///Resolver resolving tags (data types).
        Resolver resolver_;
        ///Nodes associated with anchors. Used by YAML aliases.
        Node[string] 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[])[] 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[])[] nodeAppenders_;

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

        /**
         * Determine if there are any nodes left.
         *
         * Must be called before loading as it handles the stream start event.
         */
        bool checkNode() @safe
        {
            // If next event is stream start, skip it
            parser_.skipOver!"a.id == b"(EventID.streamStart);

            //True if there are more documents available.
            return parser_.front.id != 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_.front.id != 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();
        }

    private:

        void skipExpected(const EventID id) @safe
        {
            const foundExpected = parser_.skipOver!"a.id == b"(id);
            assert(foundExpected, text("Expected ", id, " not found."));
        }
        ///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)
            @safe
        {
            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() @safe
        {
            skipExpected(EventID.documentStart);

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

            skipExpected(EventID.documentEnd);

            anchors_.destroy();
            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) @safe
        {
            if(parser_.front.id == EventID.alias_)
            {
                const event = parser_.front;
                parser_.popFront();
                const anchor = event.anchor;
                enforce((anchor in anchors_) !is null,
                        new ComposerException("Found undefined alias: " ~ anchor,
                                              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,
                                              event.startMark));

                return anchors_[anchor];
            }

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

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

            switch (parser_.front.id)
            {
                case EventID.scalar:
                    result = composeScalarNode();
                    break;
                case EventID.sequenceStart:
                    result = composeSequenceNode(pairAppenderLevel, nodeAppenderLevel);
                    break;
                case EventID.mappingStart:
                    result = composeMappingNode(pairAppenderLevel, nodeAppenderLevel);
                    break;
                default: assert(false, "This code should never be reached");
            }

            if(anchor !is null)
            {
                anchors_[anchor] = result;
            }
            return result;
        }

        ///Compose a scalar node.
        Node composeScalarNode() @safe
        {
            const event = parser_.front;
            parser_.popFront();
            const tag = resolver_.resolve(NodeID.scalar, event.tag, event.value,
                                          event.implicit);

            Node node = constructNode(event.startMark, event.endMark, tag,
                                          event.value);
            node.scalarStyle = 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)
            @safe
        {
            ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
            auto nodeAppender = &(nodeAppenders_[nodeAppenderLevel]);

            const startEvent = parser_.front;
            parser_.popFront();
            const tag = resolver_.resolve(NodeID.sequence, startEvent.tag, null,
                                          startEvent.implicit);

            while(parser_.front.id != EventID.sequenceEnd)
            {
                nodeAppender.put(composeNode(pairAppenderLevel, nodeAppenderLevel + 1));
            }

            Node node = constructNode(startEvent.startMark, parser_.front.endMark,
                                          tag, nodeAppender.data.dup);
            node.collectionStyle = startEvent.collectionStyle;
            parser_.popFront();
            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) @safe
        {
            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: " ~
                                               text(node.type) ~
                                               " NOTE: line/column shows topmost parent " ~
                                               "to which the content is being merged",
                                               startMark, endMark);
            }

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

            final switch (root.nodeID)
            {
                case NodeID.mapping:
                    Node[] toMerge;
                    toMerge.reserve(root.length);
                    foreach (ref Node key, ref Node value; root)
                    {
                        if(key.type == NodeType.merge)
                        {
                            toMerge ~= value;
                        }
                        else
                        {
                            auto temp = Node.Pair(key, value);
                            pairAppender.put(temp);
                        }
                    }
                    foreach (node; toMerge)
                    {
                        pairAppender.put(flatten(node, startMark, endMark,
                                                     pairAppenderLevel + 1, nodeAppenderLevel));
                    }
                    break;
                case NodeID.sequence:
                    foreach (ref Node node; root)
                    {
                        if (node.nodeID != NodeID.mapping)
                        {
                            error(node);
                        }
                        pairAppender.put(flatten(node, startMark, endMark,
                                                     pairAppenderLevel + 1, nodeAppenderLevel));
                    }
                    break;
                case NodeID.scalar:
                case NodeID.invalid:
                    error(root);
                    break;
            }

            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)
            @safe
        {
            ensureAppendersExist(pairAppenderLevel, nodeAppenderLevel);
            const startEvent = parser_.front;
            parser_.popFront();
            const tag = resolver_.resolve(NodeID.mapping, startEvent.tag, null,
                                          startEvent.implicit);
            auto pairAppender = &(pairAppenders_[pairAppenderLevel]);

            Tuple!(Node, Mark)[] toMerge;
            while(parser_.front.id != 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.type == NodeType.merge)
                {
                    toMerge ~= tuple(pair.value, cast(Mark)parser_.front.endMark);
                }
                //Not YAMLMerge, just add the pair.
                else
                {
                    pairAppender.put(pair);
                }
            }
            foreach(node; toMerge)
            {
                merge(*pairAppender, flatten(node[0], startEvent.startMark, node[1],
                                             pairAppenderLevel + 1, nodeAppenderLevel));
            }

            auto sorted = pairAppender.data.dup.sort!((x,y) => x.key > y.key);
            if (sorted.length) {
                foreach (index, const ref value; sorted[0 .. $ - 1].enumerate)
                    if (value.key == sorted[index + 1].key) {
                        const message = () @trusted {
                            return format("Key '%s' appears multiple times in mapping (first: %s)",
                                          value.key.get!string, value.key.startMark);
                        }();
                        throw new ComposerException(message, sorted[index + 1].key.startMark);
                    }
            }

            Node node = constructNode(startEvent.startMark, parser_.front.endMark,
                                          tag, pairAppender.data.dup);
            node.collectionStyle = startEvent.collectionStyle;
            parser_.popFront();

            pairAppender.clear();
            return node;
        }
}

// Provide good error message on multiple keys (which JSON supports)
@safe unittest
{
    import dyaml.loader : Loader;

    const str = `{
    "comment": "This is a common technique",
    "name": "foobar",
    "comment": "To write down comments pre-JSON5"
}`;

    try
        auto node = Loader.fromString(str).load();
    catch (ComposerException exc)
        assert(exc.message() ==
               "Key 'comment' appears multiple times in mapping " ~
               "(first: file <unknown>,line 2,column 5)\nfile <unknown>,line 4,column 5");
}