
///Random YAML generator. Used to generate benchmarking inputs.

import std.conv;
import std.datetime;
import std.math;
import std.random;
import std.stdio;
import std.string;
import dyaml.all;


immutable alphabet = " abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_";
immutable digits   = "0123456789";

Node config;
Node function(bool)[string] generators;
auto typesScalar     = ["string", "int", "float", "bool", "timestamp", "binary"];
auto typesScalarKey  = ["string", "int", "float", "timestamp"];
auto typesCollection = ["map","omap", "pairs", "seq", "set"];
ulong minNodesDocument;
ulong totalNodes;

static this()
{
    generators["string"]    = &genString;
    generators["int"]       = &genInt;
    generators["float"]     = &genFloat;
    generators["bool"]      = &genBool;
    generators["timestamp"] = &genTimestamp;
    generators["binary"]    = &genBinary;
    generators["map"]       = &genMap;
    generators["omap"]      = &genOmap;
    generators["pairs"]     = &genPairs;
    generators["seq"]       = &genSeq;
    generators["set"]       = &genSet;
}

real randomNormalized(const string distribution = "linear")
{
    auto generator = Random(unpredictableSeed());
    const r = uniform!"[]"(0.0L, 1.0L, generator);
    switch(distribution)
    {
        case "linear":
            return r;
        case "quadratic":
            return r * r;
        case "cubic":
            return r * r * r;
        default:
            writeln("Unknown random distribution: ", distribution,
                    ", falling back to linear");
            return randomNormalized("linear");
    }
}

long randomLong(const long min, const long max, const string distribution = "linear")
{
    return min + cast(long)round((max - min) * randomNormalized(distribution));
}

real randomReal(const real min, const real max, const string distribution = "linear")
{
    return min + (max - min) * randomNormalized(distribution);
}

char randomChar(const string chars)
{
    return chars[randomLong(0, chars.length - 1)];
}

string randomType(string[] types)
{
    auto probabilities = new uint[types.length];
    foreach(index, type; types)
    {
        probabilities[index] = config[type]["probability"].as!uint;
    }
    return types[dice(probabilities)];
}

Node genString(bool root = false)
{
    auto range = config["string"]["range"];

    const chars = randomLong(range["min"].as!uint, range["max"].as!uint,
                             range["dist"].as!string);

    char[] result = new char[chars];
    result[0] = randomChar(alphabet);
    foreach(i; 1 .. chars)
    {
        result[i] = randomChar(alphabet ~ digits);
    }

    return Node(cast(string)result);
}

Node genInt(bool root = false)
{
    auto range = config["int"]["range"];

    const result = randomLong(range["min"].as!int, range["max"].as!int,
                              range["dist"].as!string);

    return Node(result);
}

Node genFloat(bool root = false)
{
    auto range = config["float"]["range"];

    const result = randomReal(range["min"].as!real, range["max"].as!real,
                              range["dist"].as!string);

    return Node(result);
}

Node genBool(bool root = false)
{
    return Node([true, false][randomLong(0, 1)]);
}

Node genTimestamp(bool root = false)
{
    auto range = config["timestamp"]["range"];

    auto hnsecs = randomLong(range["min"].as!ulong, range["max"].as!ulong,
                             range["dist"].as!string);

    if(randomNormalized() <= config["timestamp"]["round-chance"].as!real)
    {
        hnsecs -= hnsecs % 10000000;
    }

    return Node(SysTime(hnsecs));
}

Node genBinary(bool root = false)
{
    auto range = config["binary"]["range"];

    const bytes = randomLong(range["min"].as!uint, range["max"].as!uint,
                             range["dist"].as!string);

    ubyte[] result = new ubyte[bytes];
    foreach(i; 0 .. bytes)
    {
        result[i] = cast(ubyte)randomLong(0, 255);
    }

    return Node(result);
}

Node nodes(const bool root, Node range, const string tag, const bool set = false)
{
    auto types = config["collection-keys"].as!bool ? typesCollection : [];
    types ~= (set ? typesScalarKey : typesScalar);

    Node[] nodes;
    if(root)
    {
        while(!(totalNodes >= minNodesDocument))
        {
            nodes ~= generateNode(randomType(types));
        }
    }
    else
    {
        const elems = randomLong(range["min"].as!uint, range["max"].as!uint,
                                 range["dist"].as!string);

        nodes = new Node[elems];
        foreach(i; 0 .. elems)
        {
            nodes[i] = generateNode(randomType(types));
        }
    }

    return Node(nodes, tag);
}

Node genSeq(bool root = false)
{
    return nodes(root, config["seq"]["range"], "tag:yaml.org,2002:seq");
}

Node genSet(bool root = false)
{
    return nodes(root, config["seq"]["range"], "tag:yaml.org,2002:set", true);
}

Node pairs(bool root, bool complex, Node range, string tag)
{
    Node[] keys, values;

    if(root)
    {
        while(!(totalNodes >= minNodesDocument))
        {
            keys ~= generateNode(randomType(typesScalarKey ~ (complex ? typesCollection : [])));
            values ~= generateNode(randomType(typesScalar ~ typesCollection));
        }
    }
    else
    {
        const pairs = randomLong(range["min"].as!uint, range["max"].as!uint,
                                 range["dist"].as!string);

        keys = new Node[pairs];
        values = new Node[pairs];
        foreach(i; 0 .. pairs)
        {
            keys[i] = generateNode(randomType(typesScalarKey ~ (complex ? typesCollection : [])));
            values[i] = generateNode(randomType(typesScalar ~ typesCollection));
        }
    }

    return Node(keys, values, tag);
}

Node genMap(bool root = false)
{
    Node range = config["map"]["range"];
    const complex = config["complex-keys"].as!bool;

    return pairs(root, complex, range, "tag:yaml.org,2002:map");
}

Node genOmap(bool root = false)
{
    Node range = config["omap"]["range"];
    const complex = config["complex-keys"].as!bool;

    return pairs(root, complex, range, "tag:yaml.org,2002:omap");
}

Node genPairs(bool root = false)
{
    Node range = config["pairs"]["range"];
    const complex = config["complex-keys"].as!bool;

    return pairs(root, complex, range, "tag:yaml.org,2002:pairs");
}

Node generateNode(const string type, bool root = false)
{
    ++totalNodes;
    return generators[type](root);
}

Node[] generate(const string configFileName)
{
    config = Loader(configFileName).load();

    minNodesDocument = config["min-nodes-per-document"].as!long;

    Node[] result;
    foreach(i; 0 .. config["documents"].as!uint)
    {
        result ~= generateNode(config["root-type"].as!string, true);
        totalNodes = 0;
    }

    return result;
}


void main(string[] args)
{
    //Help message.
    if(args.length == 1)
    {
        writeln("Usage: yaml_gen FILE [CONFIG_FILE]\n");
        writeln("Generates a random YAML file and writes it to FILE.");
        writeln("If provided, CONFIG_FILE overrides the default config file.");
        return;
    }

    string configFile = args.length >= 3 ? args[2] : "config.yaml";

    try
    {
        //Generate and dump the nodes.
        Node[] generated = generate(configFile);

        auto dumper     = Dumper(args[1]);
        auto encoding   = config["encoding"];
        dumper.encoding = encoding == "utf-16" ? Encoding.UTF_16:
                          encoding == "utf-32" ? Encoding.UTF_32:
                                                 Encoding.UTF_8;

        dumper.indent = config["indent"].as!uint;
        dumper.textWidth = config["text-width"].as!uint;
        dumper.dump(generated);
    }
    catch(YAMLException e)
    {
        writeln("ERROR: ", e.msg);
    }
}
