// This file contains test helpers to generate streams of various types of data.

//
// Generates a stream of normal documents, with all the different BSON types that are representable
// from the shell.
//
// Interface:
//
// next() // Get the next document in the stream
// hasNext() // Check if the stream has any more documents
//
// Usage:
//
// var generator = new DataGenerator();
// while (generator.hasNext()) {
//     var nextDoc = generator.next();
//     // Do something with nextDoc
// }
//
function DataGenerator() {

    var hexChars = "0123456789abcdefABCDEF";
    var regexOptions = "igm";
    var stringChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    var base64Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";

    // Generator functions
    // BSON Type: -1
    function GenMinKey (seed) {
        return MinKey()
    }
    // BSON Type: 0 (EOO)
    // No Shell Equivalent
    // BSON Type: 1
    function GenNumberDouble (seed) {
        var seed = seed || 0;
        return Number(seed)
    }
    // BSON Type: 2
    function GenString (seed) {
        var seed = seed || 0;
        var text = "";

        for (var i=0; i < (seed % 1000) + 1; i++) {
            text += stringChars.charAt((seed + (i % 10)) % stringChars.length);
        }

        return text;
    }
    // Javascript Dates get stored as strings
    function GenDate (seed) {
        // The "Date" constructor without "new" ignores its arguments anyway, so don't bother
        // using the seed.
        return Date();
    }
    // BSON Type: 3
    function GenObject (seed) {
        var seed = seed || 0;

        return { "object" : true };
    }
    // BSON Type: 4
    function GenArray (seed) {
        var seed = seed || 0;

        return [ "array", true ];
    }
    // BSON Type: 5
    function GenBinData (seed) {
        var seed = seed || 0;

        var text = "";

        for (var i=0; i < (seed % 1000) + 1; i++) {
            text += base64Chars.charAt((seed + (i % 10)) % base64Chars.length);
        }

        while ((text.length % 4) != 0) {
            text += "=";
        }

        return BinData(seed % 6, text);
    }
    // BSON Type: 6
    function GenUndefined (seed) {
        return undefined;
    }
    // BSON Type: 7
    function GenObjectId (seed) {
        var seed = seed || 0;
        var hexString = "";

        for (var i=0; i < 24; i++) {
            hexString += hexChars.charAt((seed + (i % 10)) % hexChars.length);
        }

        return ObjectId(hexString);
    }
    // BSON Type: 8
    function GenBool (seed) {
        var seed = seed || 0;

        return (seed % 2) === 0;
    }
    // BSON Type: 9
    // Our ISODate constructor equals the Date BSON type
    function GenISODate (seed) {
        var seed = seed || 0;

        var year = (seed % (2037 - 1970)) + 1970;
        var month = (seed % 12) + 1;
        var day = (seed % 27) + 1;
        var hour = seed % 24;
        var minute = seed % 60;
        var second = seed % 60;
        var millis = seed % 1000;

        function pad(number, length) {

            var str = '' + number;

            while (str.length < length) {
                str = '0' + str;
            }

            return str;
        }

        return ISODate(pad(year, 4) + "-" + pad(month, 2) + "-" + pad(day, 2) + "T" +
                       pad(hour, 2) + ":" + pad(minute, 2) + ":" + pad(second, 2) + "." + pad(millis, 3));
    }
    // BSON Type: 10
    function GenNull (seed) {
        return null;
    }
    // BSON Type: 11
    function GenRegExp (seed) {
        var seed = seed || 0;
        var options = "";

        for (var i=0; i < (seed % 3) + 1; i++) {
            options += regexOptions.charAt((seed + (i % 10)) % regexOptions.length);
        }

        return RegExp(GenString(seed), options);
    }
    function GenRegExpLiteral (seed) {
        // We can't pass variables to a regex literal, so we can't programmatically generate the
        // data.  Instead we rely on the "RegExp" constructor.
        return /regexliteral/;
    }
    // BSON Type: 12
    // The DBPointer type in the shell equals the DBRef BSON type
    function GenDBPointer (seed) {
        var seed = seed || 0;

        return DBPointer(GenString(seed), GenObjectId(seed));
    }
    // BSON Type: 13 (Code)
    // No Shell Equivalent
    // BSON Type: 14 (Symbol)
    // No Shell Equivalent
    // BSON Type: 15 (CodeWScope)
    // No Shell Equivalent
    // BSON Type: 16
    function GenNumberInt (seed) {
        var seed = seed || 0;

        return NumberInt(seed);
    }
    // BSON Type: 17
    function GenTimestamp (seed) {
        var seed = seed || 0;

        // Make sure our timestamp is not zero, because that doesn't round trip from 2.4 to latest.
        // See SERVER-12302.
        if (seed == 0) {
            seed = 1;
        }

        return Timestamp(seed, (seed * 100000) / 99999);
    }
    // BSON Type: 18
    function GenNumberLong (seed) {
        var seed = seed || 0;

        return NumberLong(seed)
    }
    // BSON Type: 127
    function GenMaxKey (seed) {
        return MaxKey()
    }
    // The DBRef type is not a BSON type but is treated specially in the shell:
    function GenDBRef (seed) {
        var seed = seed || 0;

        return DBRef(GenString(seed), GenObjectId(seed));
    }

    function GenFlatObjectAllTypes(seed) {
        return {
            "MinKey" : GenMinKey(seed),
            "NumberDouble" : GenNumberDouble(seed),
            "String" : GenString(seed),
            // Javascript Dates get stored as strings
            "Date" : GenDate(seed),
            // BSON Type: 3
            "Object" : GenObject(seed),
            // BSON Type: 4
            "Array" : GenArray(seed),
            // BSON Type: 5
            "BinData" : GenBinData(seed),
            // BSON Type: 6
            "Undefined" : undefined,
            // BSON Type: 7
            "jstOID" : GenObjectId(seed),
            // BSON Type: 8
            "Bool" : GenBool(seed),
            // BSON Type: 9
            // Our ISODate constructor equals the Date BSON type
            "ISODate" : GenISODate(seed),
            // BSON Type: 10
            "jstNULL" : GenNull(seed),
            // BSON Type: 11
            "RegExp" : GenRegExp(seed),
            "RegExpLiteral" : GenRegExpLiteral(seed),
            // BSON Type: 12
            // The DBPointer type in the shell equals the DBRef BSON type
            "DBPointer" : GenDBPointer(seed),
            // BSON Type: 13 (Code)
            // No Shell Equivalent
            // BSON Type: 14 (Symbol)
            // No Shell Equivalent
            // BSON Type: 15 (CodeWScope)
            // No Shell Equivalent
            // BSON Type: 16
            "NumberInt" : GenNumberInt(seed),
            // BSON Type: 17
            "Timestamp" : GenTimestamp(seed),
            // BSON Type: 18
            "NumberLong" : GenNumberLong(seed),
            // BSON Type: 127
            "MaxKey" : GenMaxKey(seed),
            // The DBRef type is not a BSON type but is treated specially in the shell:
            "DBRef" : GenDBRef(seed),
        }
    }

    function GenFlatObjectAllTypesHardCoded() {
        return {
            // BSON Type: -1
            "MinKey" : MinKey(),
            // BSON Type: 0 (EOO)
            // No Shell Equivalent
            // BSON Type: 1
            "NumberDouble" : Number(4.0),
            // BSON Type: 2
            "String" : "string",
            // Javascript Dates get stored as strings
            "Date" : Date("2013-12-11T19:38:24.055Z"),
            "Date2" : GenDate(10000),
            // BSON Type: 3
            "Object" : { "object" : true },
            // BSON Type: 4
            "Array" : [ "array", true ],
            // BSON Type: 5
            "BinData" : BinData(0, "aaaa"),
            // BSON Type: 6
            "Undefined" : undefined,
            // BSON Type: 7
            "jstOID" : ObjectId("aaaaaaaaaaaaaaaaaaaaaaaa"),
            // BSON Type: 8
            "Bool" : true,
            // BSON Type: 9
            // Our ISODate constructor equals the Date BSON type
            "ISODate" : ISODate("2013-12-11T19:38:24.055Z"),
            // BSON Type: 10
            "jstNULL" : null,
            // BSON Type: 11
            "RegEx" : RegExp("a"),
            "RegEx" : /a/,
            // BSON Type: 12
            // The DBPointer type in the shell equals the DBRef BSON type
            "DBPointer" : DBPointer("foo", ObjectId("bbbbbbbbbbbbbbbbbbbbbbbb")),
            // BSON Type: 13 (Code)
            // No Shell Equivalent
            // BSON Type: 14 (Symbol)
            // No Shell Equivalent
            // BSON Type: 15 (CodeWScope)
            // No Shell Equivalent
            // BSON Type: 16
            "NumberInt" : NumberInt(5),
            // BSON Type: 17
            "Timestamp" : Timestamp(1,2),
            // BSON Type: 18
            "NumberLong" : NumberLong(6),
            // BSON Type: 127
            "MaxKey" : MaxKey(),
            // The DBRef type is not a BSON type but is treated specially in the shell:
            "DBRef" : DBRef("bar", 2)
        }
    }

    // Data we are using as a source for our testing
    testData = [
        GenFlatObjectAllTypesHardCoded(),
        GenFlatObjectAllTypes(0),
        GenFlatObjectAllTypes(2),
        GenFlatObjectAllTypes(3),
        GenFlatObjectAllTypes(5),
        GenFlatObjectAllTypes(7),
        GenFlatObjectAllTypes(23),
        GenFlatObjectAllTypes(111),
        GenFlatObjectAllTypes(11111111),
    ]

    // Cursor interface
    var i = 0;
    return {
        "hasNext" : function () {
            return i < testData.length;
        },
        "next" : function () {
            if (i >= testData.length) {
                return undefined;
            }
            return testData[i++];
        }
    }
}

//
// Generates a stream of index data documents, with a few different attributes that are valid for
// the different index types.
//
// Interface:
//
// next() // Get the next document in the stream
// hasNext() // Check if the stream has any more documents
//
// Returns documents of the form:
//
// {
//     "spec" : <index spec>,
//     "options" : <index options>
// }
//
// Usage:
//
// var generator = new IndexDataGenerator();
// while (generator.hasNext()) {
//     var nextIndexDocument = generator.next();
//     var nextIndexSpec = nextIndexDocument["spec"];
//     var nextIndexOptions = nextIndexDocument["options"];
//     db.ensureIndex(nextIndexSpec, nextIndexOptions);
// }
//
function IndexDataGenerator(options) {

    // getNextUniqueKey()
    //
    // This function returns a new key each time it is called and is guaranteed to not return
    // duplicates.
    //
    // The sequence of values returned is a-z then A-Z.  When "Z" is reached, a new character is added
    // and the first one wraps around, resulting in "aa".  The process is repeated, so we get a sequence
    // like this:
    //
    // "a"
    // "b"
    // ...
    // "z"
    // "A"
    // ...
    // "Z"
    // "aa"
    // "ba"
    // ...
    var currentKey = "";
    function getNextUniqueKey() {

        function setCharAt(str, index, chr) {
            if (index > str.length-1) {
                return str;
            }
            return str.substr(0, index) + chr + str.substr(index + 1);
        }

        // Index of the letter we are advancing in our current key
        var currentKeyIndex = 0;
        var keyChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";

        do {
            // If we are advancing a letter that does not exist yet, append a new character and
            // return the result.  For example, this is the case where "aa" follows "Z".
            if (currentKeyIndex + 1 > currentKey.length) {
                currentKey += keyChars[0];
                return currentKey;
            }

            // Find the character (index into keyChars) that we currently have at this position, set
            // this position to the next character in the keyChars sequence
            keyCharsIndex = keyChars.search(currentKey[currentKeyIndex]);
            currentKey = setCharAt(currentKey, currentKeyIndex,
                                   keyChars[(keyCharsIndex + 1) % keyChars.length]);
            currentKeyIndex = currentKeyIndex + 1;

            // Loop again if we advanced the character past the end of keyChars and wrapped around,
            // so that we can advance the next character over too.  For example, this is the case
            // where "ab" follows "Za".
        } while (keyCharsIndex + 1 >= keyChars.length)

        return currentKey;
    }

    //
    // Index Generation
    //

    function GenSingleFieldIndex(seed) {
        var index = {};
        index[getNextUniqueKey()] = (seed % 2) == 1 ? 1 : -1;
        return index;
    }

    function GenCompoundIndex(seed) {
        var index = {};
        var i;
        for (i = 0; i < (seed % 2) + 2; i++) {
            index[getNextUniqueKey()] = ((seed + i) % 2) == 1 ? 1 : -1;
        }
        return index;
    }

    function GenNestedIndex(seed) {
        var index = {};
        var i;
        var key = getNextUniqueKey();
        for (i = 0; i < (seed % 2) + 1; i++) {
            key += "." + getNextUniqueKey();
        }
        index[key] = (seed % 2) == 1 ? 1 : -1;
        return index;
    }

    function Gen2dsphereIndex(seed) {
        var index = {};
        index[getNextUniqueKey()] = "2dsphere";
        return index;
    }

    function Gen2dIndex(seed) {
        var index = {};
        index[getNextUniqueKey()] = "2d";
        return index;
    }

    function GenHaystackIndex(seed) {
        var index = {};
        index[getNextUniqueKey()] = "geoHaystack";
        // Haystack indexes need a non geo field, and the geo field must be first
        index[getNextUniqueKey()] = (seed % 2) == 1 ? 1 : -1;
        return index;
    }

    function GenTextIndex(seed) {
        var index = {};
        index[getNextUniqueKey()] = "text";
        return index;
    }

    function GenHashedIndex(seed) {
        var index = {};
        index[getNextUniqueKey()] = "hashed";
        return index;
    }

    function GenIndexOptions(seed) {
        var attributes = {};
        var i;
        for (i = 0; i < (seed % 2) + 1; i++) {
            // Mod 3 first to make sure the property type doesn't correlate with (seed % 2)
            var propertyType = (seed % 3 + i) % 2;
            if (propertyType == 0) {
                attributes["expireAfterSeconds"] = ((seed + i) * 10000) % 9999 + 1000;
            }
            if (propertyType == 1) {
                attributes["sparse"] = true;
            }
            else {
                // TODO:  We have to test this as a separate stage because we want to round trip
                // multiple documents
                //attributes["unique"] = true;
            }
        }
        return attributes;
    }

    function Gen2dIndexOptions(seed) {
        var attributes = GenIndexOptions(seed);
        var i;
        for (i = 0; i < (seed % 2) + 1; i++) {
            // Mod 3 first to make sure the property type doesn't correlate with (seed % 2)
            var propertyType = (seed + i) % 3;
            // When using a 2d index, the following additional index properties are supported:
            // { "bits" : <bit precision>, "min" : <lower bound>, "max" : <upper bound> }
            if (propertyType == 0) {
                attributes["bits"] = ((seed + i) * 10000) % 100 + 10;
            }
            if (propertyType == 1) {
                attributes["min"] = ((seed + i) * 10000) % 100 + 10;
            }
            if (propertyType == 2) {
                attributes["max"] = ((seed + i) * 10000) % 100 + 10;
            }
            else {
            }
        }
        return attributes;
    }

    function GenHaystackIndexOptions(seed) {
        var attributes = GenIndexOptions(seed);
        // When using a haystack index, the following additional index properties are required:
        // { "bucketSize" : <bucket value> }
        attributes["bucketSize"] = (seed * 10000) % 100 + 10;
        return attributes;
    }

    function GenTextIndexOptions(seed) {
        var attributes = GenIndexOptions(seed);
        // When using a text index, the following additional index properties are required when
        // downgrading from 2.6:
        // { "textIndexVersion" : 1 }
        attributes["textIndexVersion"] = 1
        return attributes;
    }

    function Gen2dSphereIndexOptions(seed) {
        var attributes = GenIndexOptions(seed);
        // When using a 2dsphere index, the following additional index properties are required when
        // downgrading from 2.6:
        // { "2dsphereIndexVersion" : 1 }
        attributes["2dsphereIndexVersion"] = 1
        return attributes;
    }

    testIndexes = [
        // Single Field Indexes
        { "spec" : GenSingleFieldIndex(1), "options" : GenIndexOptions(0) },
        { "spec" : GenSingleFieldIndex(0), "options" : GenIndexOptions(1) },

        // Compound Indexes
        { "spec" : GenCompoundIndex(0), "options" : GenIndexOptions(2) },
        { "spec" : GenCompoundIndex(1), "options" : GenIndexOptions(3) },
        { "spec" : GenCompoundIndex(2), "options" : GenIndexOptions(4) },
        { "spec" : GenCompoundIndex(3), "options" : GenIndexOptions(5) },
        { "spec" : GenCompoundIndex(4), "options" : GenIndexOptions(6) },
        { "spec" : GenCompoundIndex(5), "options" : GenIndexOptions(7) },
        { "spec" : GenCompoundIndex(6), "options" : GenIndexOptions(8) },

        // Multikey Indexes
        // (Same index spec as single field)

        // Nested Indexes
        { "spec" : GenNestedIndex(0), "options" : GenIndexOptions(9) },
        { "spec" : GenNestedIndex(1), "options" : GenIndexOptions(10) },
        { "spec" : GenNestedIndex(2), "options" : GenIndexOptions(11) },

        // Geospatial Indexes
        //   2dsphere
        { "spec" : Gen2dsphereIndex(7), "options" : Gen2dSphereIndexOptions(12) },
        //   2d
        { "spec" : Gen2dIndex(8), "options" : Gen2dIndexOptions(13) },
        //   Haystack
        { "spec" : GenHaystackIndex(9), "options" : GenHaystackIndexOptions(13) },

        // Text Indexes
        { "spec" : GenTextIndex(10), "options" : GenTextIndexOptions(14) },

        // Hashed Index
        { "spec" : GenHashedIndex(10), "options" : GenIndexOptions(14) },
    ]

    // Cursor interface
    var i = 0;
    return {
        "hasNext" : function () {
            return i < testIndexes.length;
        },
        "next" : function () {
            if (i >= testIndexes.length) {
                return undefined;
            }
            return testIndexes[i++];
        }
    }
}

//
// Generates a collection metadata object
//
// Interface:
//
// get() // Get a collection metadata object, based on the given options
//
// Options:
//
// {
//     "capped" : (true/false) // Return all capped collection metadata
// }
//
// Usage:
//
// var generator = new CollectionMetadataGenerator({ "capped" : true });
// var metadata = generator.get();
//
function CollectionMetadataGenerator(options) {

    var capped = true;
    var options = options || {};

    for (var option in options) {
        if (options.hasOwnProperty(option)) {
            if (option === 'capped') {
                if (typeof(options['capped']) !== 'boolean') {
                    throw "\"capped\" options must be boolean in CollectionMetadataGenerator";
                }
                capped = options['capped'];
            }
            else {
                throw "Unsupported key in options passed to CollectionMetadataGenerator: " + option;
            }
        }
    }

    // Collection metadata we are using as a source for testing
    //db.createCollection(name, {capped: <Boolean>, autoIndexId: <Boolean>, size: <number>, max <number>} )
    var cappedCollectionMetadata = {
        "capped" : true,
        "size" : 100000,
        "max" : 2000,
        "usePowerOf2Sizes" : true,
        //"autoIndexId" : false // XXX: this doesn't exist in 2.4
    }
    // We need to explicitly enable usePowerOf2Sizes, since it's the default in 2.6 but not in 2.4
    var normalCollectionMetadata = {
        "usePowerOf2Sizes" : true
    };

    return {
        "get" : function () {
            return capped ? cappedCollectionMetadata : normalCollectionMetadata;
        }
    }
}

//
// Wrapper for the above classes useful for passing into functions that expect a data generator
//
function CollectionDataGenerator(options) {
    return {
        "data" : new DataGenerator(),
        "indexes" : new IndexDataGenerator(),
        "collectionMetadata" : new CollectionMetadataGenerator(options)
    }
}
