
//          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)

/**
 * YAML node _representer. Prepares YAML nodes for output. A tutorial can be 
 * found $(LINK2 ../tutorials/custom_types.html, here).    
 *
 * Code based on $(LINK2 http://www.pyyaml.org, PyYAML).
 */
module dyaml.representer;


import std.algorithm;
import std.array;
import std.base64;
import std.container;
import std.conv;
import std.datetime;
import std.exception;
import std.format;
import std.math;
import std.stream;
import std.typecons;
import std.string;

import dyaml.exception;
import dyaml.node;
import dyaml.serializer;
import dyaml.style;
import dyaml.tag;


///Exception thrown on Representer errors.
class RepresenterException : YAMLException
{
    mixin ExceptionCtors;
}

/**
 * Represents YAML nodes as scalar, sequence and mapping nodes ready for output.
 *
 * This class is used to add support for dumping of custom data types.
 *
 * It can also override default node formatting styles for output.
 */
final class Representer
{
    private:
        ///Representer functions indexed by types.
        Node function(ref Node, Representer)[TypeInfo] representers_;
        ///Default style for scalar nodes.
        ScalarStyle defaultScalarStyle_ = ScalarStyle.Invalid;
        ///Default style for collection nodes.
        CollectionStyle defaultCollectionStyle_ = CollectionStyle.Invalid;

    public:
        @disable bool opEquals(ref Representer);
        @disable int opCmp(ref Representer);

        /**
         * Construct a Representer.
         * 
         * Params:  useDefaultRepresenters = Use default representer functions
         *                                   for default YAML types? This can be
         *                                   disabled to use custom representer
         *                                   functions for default types.
         */
        this(const Flag!"useDefaultRepresenters" useDefaultRepresenters = Yes.useDefaultRepresenters) 
            @safe
        {
            if(!useDefaultRepresenters){return;}
            addRepresenter!YAMLNull(&representNull);
            addRepresenter!string(&representString);
            addRepresenter!(ubyte[])(&representBytes);
            addRepresenter!bool(&representBool);
            addRepresenter!long(&representLong);
            addRepresenter!real(&representReal);
            addRepresenter!(Node[])(&representNodes);
            addRepresenter!(Node.Pair[])(&representPairs);
            addRepresenter!SysTime(&representSysTime);
        }

        ///Destroy the Representer.
        pure @safe nothrow ~this()
        {
            clear(representers_);
            representers_ = null;
        }

        ///Set default _style for scalars. If style is $(D ScalarStyle.Invalid), the _style is chosen automatically.
        @property void defaultScalarStyle(ScalarStyle style) pure @safe nothrow
        {
            defaultScalarStyle_ = style;
        }

        ///Set default _style for collections. If style is $(D CollectionStyle.Invalid), the _style is chosen automatically. 
        @property void defaultCollectionStyle(CollectionStyle style) pure @safe nothrow
        {
            defaultCollectionStyle_ = style;
        }

        /**
         * Add a function to represent nodes with a specific data type.
         *
         * The representer function takes references to a $(D Node) storing the data
         * type and to the $(D Representer). It returns the represented node and may
         * throw a $(D RepresenterException). See the example for more information.
         * 
         *
         * Only one function may be specified for one data type. Default data 
         * types already have representer functions unless disabled in the
         * $(D Representer) constructor.
         *
         *
         * Structs and classes must implement the $(D opCmp()) operator for D:YAML 
         * support. The signature of the operator that must be implemented 
         * is $(D const int opCmp(ref const MyStruct s)) for structs where 
         * $(I MyStruct) is the struct type, and $(D int opCmp(Object o)) for 
         * classes. Note that the class $(D opCmp()) should not alter the compared
         * values - it is not const for compatibility reasons.
         *
         * Params:  representer = Representer function to add.
         *
         * Examples:
         *
         * Representing a simple struct:
         * --------------------
         * import std.string;
         *
         * import yaml;
         *
         * struct MyStruct
         * {
         *     int x, y, z;
         *
         *     //Any D:YAML type must have a custom opCmp operator.
         *     //This is used for ordering in mappings.
         *     const int opCmp(ref const MyStruct s)
         *     {
         *         if(x != s.x){return x - s.x;}
         *         if(y != s.y){return y - s.y;}
         *         if(z != s.z){return z - s.z;}
         *         return 0;
         *     }
         * }
         *
         * Node representMyStruct(ref Node node, Representer representer)
         * { 
         *     //The node is guaranteed to be MyStruct as we add representer for MyStruct.
         *     auto value = node.as!MyStruct;
         *     //Using custom scalar format, x:y:z.
         *     auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
         *     //Representing as a scalar, with custom tag to specify this data type.
         *     return representer.representScalar("!mystruct.tag", scalar);
         * }
         *
         * void main()
         * {
         *     auto dumper = Dumper("file.yaml");
         *     auto representer = new Representer;
         *     representer.addRepresenter!MyStruct(&representMyStruct);
         *     dumper.representer = representer;
         *     dumper.dump(Node(MyStruct(1,2,3)));
         * }
         * --------------------
         *
         * Representing a class:
         * --------------------
         * import std.string;
         *
         * import yaml;
         *
         * class MyClass
         * {
         *     int x, y, z;
         *
         *     this(int x, int y, int z)
         *     {
         *         this.x = x; 
         *         this.y = y; 
         *         this.z = z;
         *     }
         *
         *     //Any D:YAML type must have a custom opCmp operator.
         *     //This is used for ordering in mappings.
         *     override int opCmp(Object o)
         *     {
         *         MyClass s = cast(MyClass)o;
         *         if(s is null){return -1;}
         *         if(x != s.x){return x - s.x;}
         *         if(y != s.y){return y - s.y;}
         *         if(z != s.z){return z - s.z;}
         *         return 0;
         *     }
         *
         *     ///Useful for Node.as!string .
         *     override string toString()
         *     {
         *         return format("MyClass(%s, %s, %s)", x, y, z);
         *     }
         * }
         *
         * //Same as representMyStruct.
         * Node representMyClass(ref Node node, Representer representer)
         * { 
         *     //The node is guaranteed to be MyClass as we add representer for MyClass.
         *     auto value = node.as!MyClass;
         *     //Using custom scalar format, x:y:z.
         *     auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
         *     //Representing as a scalar, with custom tag to specify this data type.
         *     return representer.representScalar("!myclass.tag", scalar);
         * }
         *
         * void main()
         * {
         *     auto dumper = Dumper("file.yaml");
         *     auto representer = new Representer;
         *     representer.addRepresenter!MyClass(&representMyClass);
         *     dumper.representer = representer;
         *     dumper.dump(Node(new MyClass(1,2,3)));
         * }
         * --------------------
         */
        void addRepresenter(T)(Node function(ref Node, Representer) representer) @trusted
        {
            assert((typeid(T) in representers_) is null, 
                   "Representer function for data type " ~ typeid(T).toString() ~
                   " already specified. Can't specify another one");
            representers_[typeid(T)] = representer;
        }

        //If profiling shows a bottleneck on tag construction in these 3 methods,
        //we'll need to take Tag directly and have string based wrappers for 
        //user code.

        /**
         * Represent a _scalar with specified _tag.
         *
         * This is used by representer functions that produce scalars.
         *
         * Params:  tag    = Tag of the _scalar.
         *          scalar = Scalar value.
         *          style  = Style of the _scalar. If invalid, default _style will be used.
         *                   If the node was loaded before, previous _style will always be used.
         *
         * Returns: The represented node.
         *
         * Example:
         * --------------------
         * struct MyStruct
         * {
         *     int x, y, z;
         *
         *     //Any D:YAML type must have a custom opCmp operator.
         *     //This is used for ordering in mappings.
         *     const int opCmp(ref const MyStruct s)
         *     {
         *         if(x != s.x){return x - s.x;}
         *         if(y != s.y){return y - s.y;}
         *         if(z != s.z){return z - s.z;}
         *         return 0;
         *     }        
         * }
         *
         * Node representMyStruct(ref Node node, Representer representer)
         * { 
         *     auto value = node.as!MyStruct;
         *     auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
         *     return representer.representScalar("!mystruct.tag", scalar);
         * }
         * --------------------
         */
        Node representScalar(string tag, string scalar, 
                             ScalarStyle style = ScalarStyle.Invalid) @safe
        {
            if(style == ScalarStyle.Invalid){style = defaultScalarStyle_;}
            return Node.rawNode(Node.Value(scalar), Mark(), Tag(tag), style,
                                CollectionStyle.Invalid);
        }

        /**
         * Represent a _sequence with specified _tag, representing children first.
         *
         * This is used by representer functions that produce sequences.
         *
         * Params:  tag      = Tag of the _sequence.
         *          sequence = Sequence of nodes.
         *          style    = Style of the _sequence. If invalid, default _style will be used.
         *                     If the node was loaded before, previous _style will always be used.
         *
         * Returns: The represented node.
         *
         * Throws:  $(D RepresenterException) if a child could not be represented.
         *
         * Example:
         * --------------------
         * struct MyStruct
         * {
         *     int x, y, z;
         *
         *     //Any D:YAML type must have a custom opCmp operator.
         *     //This is used for ordering in mappings.
         *     const int opCmp(ref const MyStruct s)
         *     {
         *         if(x != s.x){return x - s.x;}
         *         if(y != s.y){return y - s.y;}
         *         if(z != s.z){return z - s.z;}
         *         return 0;
         *     }        
         * }
         *
         * Node representMyStruct(ref Node node, Representer representer)
         * { 
         *     auto value = node.as!MyStruct;
         *     auto nodes = [Node(value.x), Node(value.y), Node(value.z)];
         *     //use flow style
         *     return representer.representSequence("!mystruct.tag", nodes,
         *                                          CollectionStyle.Flow);
         * }
         * --------------------
         */
        Node representSequence(string tag, Node[] sequence, 
                               CollectionStyle style = CollectionStyle.Invalid) @trusted
        {
            Node[] value;
            value.length = sequence.length;

            auto bestStyle = CollectionStyle.Flow;
            foreach(idx, ref item; sequence)
            {
                value[idx] = representData(item);
                const isScalar = value[idx].isScalar;
                const s = value[idx].scalarStyle;
                if(!isScalar || (s != ScalarStyle.Invalid && s != ScalarStyle.Plain))
                {
                    bestStyle = CollectionStyle.Block;
                }
            }

            if(style == CollectionStyle.Invalid)
            {
                style = defaultCollectionStyle_ != CollectionStyle.Invalid 
                        ? defaultCollectionStyle_
                        : bestStyle;
            }
            return Node.rawNode(Node.Value(value), Mark(), Tag(tag), 
                                ScalarStyle.Invalid, style);
        }

        /**
         * Represent a mapping with specified _tag, representing children first.
         *
         * This is used by representer functions that produce mappings.
         *
         * Params:  tag   = Tag of the mapping.
         *          pairs = Key-value _pairs of the mapping.
         *          style = Style of the mapping. If invalid, default _style will be used.
         *                  If the node was loaded before, previous _style will always be used.
         *
         * Returns: The represented node.
         *
         * Throws:  $(D RepresenterException) if a child could not be represented.
         *
         * Example:
         * --------------------
         * struct MyStruct
         * {
         *     int x, y, z;
         *
         *     //Any D:YAML type must have a custom opCmp operator.
         *     //This is used for ordering in mappings.
         *     const int opCmp(ref const MyStruct s)
         *     {
         *         if(x != s.x){return x - s.x;}
         *         if(y != s.y){return y - s.y;}
         *         if(z != s.z){return z - s.z;}
         *         return 0;
         *     }        
         * }
         *
         * Node representMyStruct(ref Node node, Representer representer)
         * { 
         *     auto value = node.as!MyStruct;
         *     auto pairs = [Node.Pair("x", value.x), 
         *                   Node.Pair("y", value.y), 
         *                   Node.Pair("z", value.z)];
         *     return representer.representMapping("!mystruct.tag", pairs);
         * }
         * --------------------
         */
        Node representMapping(string tag, Node.Pair[] pairs,
                              CollectionStyle style = CollectionStyle.Invalid) @trusted
        {
            Node.Pair[] value;
            value.length = pairs.length;

            auto bestStyle = CollectionStyle.Flow;
            foreach(idx, ref pair; pairs)
            {
                value[idx] = Node.Pair(representData(pair.key), representData(pair.value));
                const keyScalar = value[idx].key.isScalar;
                const valScalar = value[idx].value.isScalar;
                const keyStyle = value[idx].key.scalarStyle;
                const valStyle = value[idx].value.scalarStyle;
                if(!keyScalar ||
                   (keyStyle != ScalarStyle.Invalid && keyStyle != ScalarStyle.Plain))
                {
                    bestStyle = CollectionStyle.Block;
                }
                if(!valScalar ||
                   (valStyle != ScalarStyle.Invalid && valStyle != ScalarStyle.Plain))
                {
                    bestStyle = CollectionStyle.Block;
                }
            }

            if(style == CollectionStyle.Invalid)
            {
                style = defaultCollectionStyle_ != CollectionStyle.Invalid 
                        ? defaultCollectionStyle_
                        : bestStyle;
            }
            return Node.rawNode(Node.Value(value), Mark(), Tag(tag), 
                                ScalarStyle.Invalid, style);
        }

    package:
        //Represent a node based on its type, and return the represented result.
        Node representData(ref Node data) @system
        {
            //User types are wrapped in YAMLObject.
            auto type = data.isUserType ? data.as!YAMLObject.type : data.type;

            enforce((type in representers_) !is null,
                    new RepresenterException("No representer function for type " 
                                             ~ type.toString() ~ " , cannot represent."));
            Node result = representers_[type](data, this);

            //Override tag if specified.
            if(!data.tag_.isNull()){result.tag_ = data.tag_;}

            //Remember style if this was loaded before.
            if(data.scalarStyle != ScalarStyle.Invalid)
            {
                result.scalarStyle = data.scalarStyle;
            }
            if(data.collectionStyle != CollectionStyle.Invalid)
            {
                result.collectionStyle = data.collectionStyle;
            }
            return result;
        }

        //Represent a node, serializing with specified Serializer.
        void represent(ref Serializer serializer, ref Node node) @trusted
        {
            auto data = representData(node);
            serializer.serialize(data);
        }
}


///Represent a _null _node as a _null YAML value.
Node representNull(ref Node node, Representer representer) @safe
{
    return representer.representScalar("tag:yaml.org,2002:null", "null");
}

///Represent a string _node as a string scalar.
Node representString(ref Node node, Representer representer) @safe
{
    string value = node.as!string;
    return value is null 
           ? representNull(node, representer) 
           : representer.representScalar("tag:yaml.org,2002:str", value);
}

///Represent a bytes _node as a binary scalar.
Node representBytes(ref Node node, Representer representer) @system
{
    const ubyte[] value = node.as!(ubyte[]);
    if(value is null){return representNull(node, representer);}
    return representer.representScalar("tag:yaml.org,2002:binary", 
                                       cast(string)Base64.encode(value),
                                       ScalarStyle.Literal);
}

///Represent a bool _node as a bool scalar.
Node representBool(ref Node node, Representer representer) @safe
{
    return representer.representScalar("tag:yaml.org,2002:bool", 
                                       node.as!bool ? "true" : "false");
}

///Represent a long _node as an integer scalar.
Node representLong(ref Node node, Representer representer) @system
{
    return representer.representScalar("tag:yaml.org,2002:int", 
                                       to!string(node.as!long));
}

///Represent a real _node as a floating point scalar.
Node representReal(ref Node node, Representer representer) @system
{
    real f = node.as!real;
    string value = isNaN(f)                  ? ".nan":
                   f == real.infinity        ? ".inf":
                   f == -1.0 * real.infinity ? "-.inf":
                   {auto a = appender!string;
                    formattedWrite(a, "%12f", f);
                    return a.data.strip();}();

    return representer.representScalar("tag:yaml.org,2002:float", value);
}

///Represent a SysTime _node as a timestamp.
Node representSysTime(ref Node node, Representer representer) @system
{
    return representer.representScalar("tag:yaml.org,2002:timestamp", 
                                       node.as!SysTime.toISOExtString());
}

///Represent a sequence _node as sequence/set.
Node representNodes(ref Node node, Representer representer) @safe
{
    auto nodes = node.as!(Node[]);
    if(node.tag_ == Tag("tag:yaml.org,2002:set"))
    {
        ///YAML sets are mapping with null values.
        Node.Pair[] pairs;
        pairs.length = nodes.length;
        Node dummy;
        foreach(idx, ref key; nodes)
        {
            pairs[idx] = Node.Pair(key, representNull(dummy, representer));
        }
        return representer.representMapping(node.tag_.get, pairs);
    }
    else
    {
        return representer.representSequence("tag:yaml.org,2002:seq", nodes);
    }
}

///Represent a mapping _node as map/ordered map/pairs.
Node representPairs(ref Node node, Representer representer) @system
{
    auto pairs = node.as!(Node.Pair[]);

    bool hasDuplicates(Node.Pair[] pairs)
    {
        //TODO this should be replaced by something with deterministic memory allocation.
        auto keys = redBlackTree!Node();
        scope(exit){clear(keys);}
        foreach(ref pair; pairs)
        {
            if(pair.key in keys){return true;}
            keys.insert(pair.key);
        }
        return false;
    }

    Node[] mapToSequence(Node.Pair[] pairs)
    {
        Node[] nodes;
        nodes.length = pairs.length;
        foreach(idx, ref pair; pairs)
        {
            nodes[idx] = representer.representMapping("tag:yaml.org,2002:map", [pair]);
        }
        return nodes;
    }

    if(node.tag_ == Tag("tag:yaml.org,2002:omap"))
    {
        enforce(!hasDuplicates(pairs),
                new RepresenterException("Duplicate entry in an ordered map"));
        return representer.representSequence(node.tag_.get, mapToSequence(pairs));
    }
    else if(node.tag_ == Tag("tag:yaml.org,2002:pairs"))
    {
        return representer.representSequence(node.tag_.get, mapToSequence(pairs));
    }
    else
    {
        enforce(!hasDuplicates(pairs),
                new RepresenterException("Duplicate entry in an unordered map"));
        return representer.representMapping("tag:yaml.org,2002:map", pairs);
    }
}

//Unittests
//These should really all be encapsulated in unittests.
private:

import dyaml.dumper;

struct MyStruct
{
    int x, y, z;

    const int opCmp(ref const MyStruct s) const pure @safe nothrow
    {
        if(x != s.x){return x - s.x;}
        if(y != s.y){return y - s.y;}
        if(z != s.z){return z - s.z;}
        return 0;
    }        
}

Node representMyStruct(ref Node node, Representer representer) @system
{ 
    //The node is guaranteed to be MyStruct as we add representer for MyStruct.
    auto value = node.as!MyStruct;
    //Using custom scalar format, x:y:z.
    auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
    //Representing as a scalar, with custom tag to specify this data type.
    return representer.representScalar("!mystruct.tag", scalar);
}

Node representMyStructSeq(ref Node node, Representer representer) @safe
{ 
    auto value = node.as!MyStruct;
    auto nodes = [Node(value.x), Node(value.y), Node(value.z)];
    return representer.representSequence("!mystruct.tag", nodes);
}

Node representMyStructMap(ref Node node, Representer representer) @safe
{ 
    auto value = node.as!MyStruct;
    auto pairs = [Node.Pair("x", value.x), 
                  Node.Pair("y", value.y), 
                  Node.Pair("z", value.z)];
    return representer.representMapping("!mystruct.tag", pairs);
}

class MyClass
{
    int x, y, z;

    this(int x, int y, int z) pure @safe nothrow
    {
        this.x = x; 
        this.y = y; 
        this.z = z;
    }
    
    override int opCmp(Object o) pure @safe nothrow
    {
        MyClass s = cast(MyClass)o;
        if(s is null){return -1;}
        if(x != s.x){return x - s.x;}
        if(y != s.y){return y - s.y;}
        if(z != s.z){return z - s.z;}
        return 0;
    }

    ///Useful for Node.as!string .
    override string toString() @trusted
    {
        return format("MyClass(%s, %s, %s)", x, y, z);
    }
}

//Same as representMyStruct.
Node representMyClass(ref Node node, Representer representer) @system
{ 
    //The node is guaranteed to be MyClass as we add representer for MyClass.
    auto value = node.as!MyClass;
    //Using custom scalar format, x:y:z.
    auto scalar = format("%s:%s:%s", value.x, value.y, value.z);
    //Representing as a scalar, with custom tag to specify this data type.
    return representer.representScalar("!myclass.tag", scalar);
}

unittest
{
    foreach(r; [&representMyStruct, 
                &representMyStructSeq, 
                &representMyStructMap])
    {
        auto dumper = Dumper(new MemoryStream());
        auto representer = new Representer;
        representer.addRepresenter!MyStruct(r);
        dumper.representer = representer;
        dumper.dump(Node(MyStruct(1,2,3)));
    }
}

unittest
{
    auto dumper = Dumper(new MemoryStream());
    auto representer = new Representer;
    representer.addRepresenter!MyClass(&representMyClass);
    dumper.representer = representer;
    dumper.dump(Node(new MyClass(1,2,3)));
}
