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

/**
 * Implements a class that resolves YAML tags. This can be used to implicitly
 * resolve tags for custom data types, removing the need to explicitly 
 * specify tags in YAML. A tutorial can be found 
 * $(LINK2 ../tutorials/custom_types.html, here).    
 * 
 * Code based on $(LINK2 http://www.pyyaml.org, PyYAML).
 */
module dyaml.resolver;


import std.conv;
import std.regex;
import std.stdio;
import std.typecons;
import std.utf;

import dyaml.node;
import dyaml.exception;
import dyaml.tag;


/**
 * Resolves YAML tags (data types).
 *
 * Can be used to implicitly resolve custom data types of scalar values.
 */
final class Resolver 
{
    private:
        ///Default tag to use for scalars.
        Tag defaultScalarTag_;
        ///Default tag to use for sequences.
        Tag defaultSequenceTag_;
        ///Default tag to use for mappings.
        Tag defaultMappingTag_;

        /**
         * Arrays of scalar resolver tuples indexed by starting character of a scalar.
         *
         * Each tuple stores regular expression the scalar must match,
         * and tag to assign to it if it matches.
         */
        Tuple!(Tag, Regex!char)[][dchar] yamlImplicitResolvers_;

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

        /**
         * Construct a Resolver.
         *
         * If you don't want to implicitly resolve default YAML tags/data types,
         * you can use defaultImplicitResolvers to disable default resolvers.
         *
         * Params:  defaultImplicitResolvers = Use default YAML implicit resolvers?
         */
        this(Flag!"useDefaultImplicitResolvers" defaultImplicitResolvers = Yes.useDefaultImplicitResolvers) 
            @safe
        {
            defaultScalarTag_   = Tag("tag:yaml.org,2002:str");
            defaultSequenceTag_ = Tag("tag:yaml.org,2002:seq");
            defaultMappingTag_  = Tag("tag:yaml.org,2002:map");
            if(defaultImplicitResolvers){addImplicitResolvers();}
        }

        ///Destroy the Resolver.
        pure @safe nothrow ~this()
        {
            yamlImplicitResolvers_.destroy();
            yamlImplicitResolvers_ = null;
        }

        /**
         * Add an implicit scalar resolver. 
         *
         * If a scalar matches regexp and starts with any character in first, 
         * its _tag is set to tag. If it matches more than one resolver _regexp
         * resolvers added _first override ones added later. Default resolvers 
         * override any user specified resolvers, but they can be disabled in
         * Resolver constructor.
         *
         * If a scalar is not resolved to anything, it is assigned the default
         * YAML _tag for strings.
         *
         * Params:  tag    = Tag to resolve to.
         *          regexp = Regular expression the scalar must match to have this _tag.
         *          first  = String of possible starting characters of the scalar.
         *
         * Examples:
         *
         * Resolve scalars starting with 'A' to !_tag :
         * --------------------
         * import std.regex;
         *
         * import dyaml.all;
         *
         * void main()
         * {
         *     auto loader = Loader("file.txt");
         *     auto resolver = new Resolver();
         *     resolver.addImplicitResolver("!tag", std.regex.regex("A.*"), "A");
         *     loader.resolver = resolver;
         *     
         *     //Note that we have no constructor from tag "!tag", so we can't
         *     //actually load anything that resolves to this tag.
         *     //See Constructor API documentation and tutorial for more information.
         *
         *     auto node = loader.load();
         * }
         * --------------------
         */
        void addImplicitResolver(string tag, Regex!char regexp, string first) 
            pure @safe 
        {
            foreach(const dchar c; first)
            {
                if((c in yamlImplicitResolvers_) is null)
                {
                    yamlImplicitResolvers_[c] = [];
                }
                yamlImplicitResolvers_[c] ~= tuple(Tag(tag), regexp);
            }
        }

    package:
        /*
         * Resolve tag of a node.
         *
         * Params:  kind     = Type of the node.
         *          tag      = Explicit tag of the node, if any.
         *          value    = Value of the node, if any.
         *          implicit = Should the node be implicitly resolved?
         *
         * If the tag is already specified and not non-specific, that tag will
         * be returned.
         *
         * Returns: Resolved tag.
         */
        Tag resolve(const NodeID kind, const Tag tag, const string value, 
                    const bool implicit) @safe 
        {
            if(!tag.isNull() && tag.get() != "!"){return tag;}

            if(kind == NodeID.Scalar)
            {
                if(!implicit){return defaultScalarTag_;}

                //Get the first char of the value.
                size_t dummy;
                const dchar first = value.length == 0 ? '\0' : decode(value, dummy);

                auto resolvers = (first in yamlImplicitResolvers_) is null ? 
                                 [] : yamlImplicitResolvers_[first];

                //If regexp matches, return tag.
                foreach(resolver; resolvers) if(!(match(value, resolver[1]).empty))
                {
                    return resolver[0];
                }
                return defaultScalarTag_;
            }
            else if(kind == NodeID.Sequence){return defaultSequenceTag_;}
            else if(kind == NodeID.Mapping) {return defaultMappingTag_;}
            assert(false, "This line of code should never be reached");
        }
        unittest
        {
            writeln("D:YAML Resolver unittest");

            auto resolver = new Resolver();

            bool tagMatch(string tag, string[] values)
            {
                Tag expected = Tag(tag);
                foreach(value; values)
                {
                    Tag resolved = resolver.resolve(NodeID.Scalar, Tag(), value, true);
                    if(expected != resolved)
                    {
                        return false;
                    }
                }
                return true;
            }

            assert(tagMatch("tag:yaml.org,2002:bool", 
                   ["yes", "NO", "True", "on"]));
            assert(tagMatch("tag:yaml.org,2002:float", 
                   ["6.8523015e+5", "685.230_15e+03", "685_230.15", 
                    "190:20:30.15", "-.inf", ".NaN"]));
            assert(tagMatch("tag:yaml.org,2002:int", 
                   ["685230", "+685_230", "02472256", "0x_0A_74_AE",
                    "0b1010_0111_0100_1010_1110", "190:20:30"]));
            assert(tagMatch("tag:yaml.org,2002:merge", ["<<"]));
            assert(tagMatch("tag:yaml.org,2002:null", ["~", "null", ""]));
            assert(tagMatch("tag:yaml.org,2002:str", 
                            ["abcd", "9a8b", "9.1adsf"]));
            assert(tagMatch("tag:yaml.org,2002:timestamp", 
                   ["2001-12-15T02:59:43.1Z",
                   "2001-12-14t21:59:43.10-05:00",
                   "2001-12-14 21:59:43.10 -5",
                   "2001-12-15 2:59:43.10",
                   "2002-12-14"]));
            assert(tagMatch("tag:yaml.org,2002:value", ["="]));
            assert(tagMatch("tag:yaml.org,2002:yaml", ["!", "&", "*"]));
        }

        ///Return default scalar tag.
        @property Tag defaultScalarTag()   const pure @safe nothrow {return defaultScalarTag_;}

        ///Return default sequence tag.
        @property Tag defaultSequenceTag() const pure @safe nothrow {return defaultSequenceTag_;}

        ///Return default mapping tag.
        @property Tag defaultMappingTag()  const pure @safe nothrow {return defaultMappingTag_;}

    private:
        ///Add default implicit resolvers.
        void addImplicitResolvers() @safe
        {
            addImplicitResolver("tag:yaml.org,2002:bool",
                                regex(r"^(?:yes|Yes|YES|no|No|NO|true|True|TRUE"
                                       "|false|False|FALSE|on|On|ON|off|Off|OFF)$"),
                                "yYnNtTfFoO");
            addImplicitResolver("tag:yaml.org,2002:float",
                                regex(r"^(?:[-+]?([0-9][0-9_]*)\\.[0-9_]*"
                                      "(?:[eE][-+][0-9]+)?|[-+]?(?:[0-9][0-9_]"
                                      "*)?\\.[0-9_]+(?:[eE][-+][0-9]+)?|[-+]?"
                                      "[0-9][0-9_]*(?::[0-5]?[0-9])+\\.[0-9_]"
                                      "*|[-+]?\\.(?:inf|Inf|INF)|\\."
                                      "(?:nan|NaN|NAN))$"),
                                "-+0123456789.");
            addImplicitResolver("tag:yaml.org,2002:int",
                                regex(r"^(?:[-+]?0b[0-1_]+"
                                       "|[-+]?0[0-7_]+"
                                       "|[-+]?(?:0|[1-9][0-9_]*)"
                                       "|[-+]?0x[0-9a-fA-F_]+"
                                       "|[-+]?[1-9][0-9_]*(?::[0-5]?[0-9])+)$"),
                                "-+0123456789");
            addImplicitResolver("tag:yaml.org,2002:merge", regex(r"^<<$"), "<");
            addImplicitResolver("tag:yaml.org,2002:null", 
                                regex(r"^$|^(?:~|null|Null|NULL)$"), "~nN\0");
            addImplicitResolver("tag:yaml.org,2002:timestamp", 
                                regex(r"^[0-9][0-9][0-9][0-9]-[0-9][0-9]-"
                                       "[0-9][0-9]|[0-9][0-9][0-9][0-9]-[0-9]"
                                       "[0-9]?-[0-9][0-9]?[Tt]|[ \t]+[0-9]"
                                       "[0-9]?:[0-9][0-9]:[0-9][0-9]"
                                       "(?:\\.[0-9]*)?(?:[ \t]*Z|[-+][0-9]"
                                       "[0-9]?(?::[0-9][0-9])?)?$"), 
                                "0123456789");
            addImplicitResolver("tag:yaml.org,2002:value", regex(r"^=$"), "=");


            //The following resolver is only for documentation purposes. It cannot work
            //because plain scalars cannot start with '!', '&', or '*'.
            addImplicitResolver("tag:yaml.org,2002:yaml", regex(r"^(?:!|&|\*)$"), "!&*");
        }
}
