/* This software is licensed under a BSD license; see the LICENSE file for details. */

AcceptabilityJudgment.name = "AcceptabilityJudgment";
AcceptabilityJudgment.obligatory = ["s", "as"];

function AcceptabilityJudgment(div, options, finishedCallback, utils) {
    var opts = {
        triggers:    [1],
        children:    [FlashSentence, {s: options.s, timeout: dget(options, "timeout", null)},
                      Question, { q:              options.q,
                                  as:             options.as,
                                  hasCorrect:     options.hasCorrect,
                                  presentAsScale: options.presentAsScale,
                                  randomOrder:    options.randomOrder,
                                  showNumbers:    options.showNumbers,
                                  timeout:        options.timeout,
                                  instructions:   options.instructions,
                                  leftComment:    options.leftComment,
                                  rightComment:   options.rightComment }]/*,
        manipulators: [
            [0, function(div) { div.style.fontSize = "larger"; return div; }]
        ]*/
    };

    return new VBox(div, opts, finishedCallback, utils);
}

AcceptabilityJudgment.htmlDescription = function (opts) {
    var s = FlashSentence.htmlDescription(opts);
    var q = Question.htmlDescription(opts);
    var p = document.createElement("p");
    var b1 = document.createElement("b");
    b1.appendChild(document.createTextNode("S: "));
    p.appendChild(b1);
    p.appendChild(document.createTextNode(opts.s + " "));
    p.appendChild(document.createElement("br"));
    var b2 = document.createElement("b");
    b2.appendChild(document.createTextNode("Q: "));
    p.appendChild(b2);
    p.appendChild(document.createTextNode(opts.q));
    return p;
}



/* This software is licensed under a BSD license; see the LICENSE file for details. */

function boolToInt(x) { if (x) return 1; else return 0; }

DashedSentence.name = "DashedSentence";
DashedSentence.obligatory = ["s"];

function DashedSentence(div, options, finishedCallback, utils) {
    this.options = options;
    if (typeof(options.s) == "string")
        this.words = options.s.split(/\s+/);
    else {
        assert_is_arraylike(options.s, "Bad value for 's' option of DashedSentence.");
        this.words = options.s;
    }
    this.mode = dget(options, "mode", "self-paced reading");
    this.wordTime = dget(options, "wordTime", 300); // Only for speeded accpetability.
    this.wordPauseTime = dget(options, "wordPauseTime", 100); // Ditto.
    this.currentWord = 0;

    // Is there a "stopping point" specified?
    this.stoppingPoint = this.words.length;
    for (var i = 0; i < this.words.length; ++i) {
        if (stringStartsWith("@", this.words[i])) {
            this.words[i] = this.words[i].substring(1);
            this.stoppingPoint = i + 1;
            break;
        }
    }

    // Defaults.
    this.unshownBorderColor = dget(options, "unshownBorderColor", "#9ea4b1");
    this.shownBorderColor = dget(options, "shownBorderColor", "black");
    this.unshownWordColor = dget(options, "unshownWordColor", "white");
    this.shownWordColor = dget(options, "shownWordColor", "black");

    // Precalculate MD5 of sentence.
    var canonicalSentence = this.words.join(' ');
    this.sentenceMD5 = hex_md5(canonicalSentence);

    this.div = div;
    this.div.className = "sentence";

    this.resultsLines = [];
    this.previousTime = null;

    this.wordDivs = new Array(this.words.length);
    for (var j = 0; j < this.words.length; ++j) {
        var div = document.createElement("div");
        div.appendChild(document.createTextNode(this.words[j]));
        this.div.appendChild(div);
        this.wordDivs[j] = div;
    }

    this.blankWord = function(w) {
        if (this.currentWord <= this.stoppingPoint) {
            this.wordDivs[w].style.borderColor = this.unshownBorderColor;
            this.wordDivs[w].style.color = this.unshownWordColor;
        }
    };
    this.showWord = function(w) {
        if (this.currentWord < this.stoppingPoint) {
            this.wordDivs[w].style.borderColor = this.shownBorderColor;
            this.wordDivs[w].style.color = this.shownWordColor;
        }
    }

    if (this.mode == "speeded acceptability") {
        this.showWord(0);
        var t = this;
        function wordTimeout() {
            t.blankWord(t.currentWord);
            ++(t.currentWord);
            if (t.currentWord >= t.stoppingPoint)
                finishedCallback([[["Sentence MD5", t.sentenceMD5]]]);
            else
                utils.setTimeout(wordPauseTimeout, t.wordPauseTime);
        }
        function wordPauseTimeout() {
            t.showWord(t.currentWord);
            utils.clearTimeout(wordPauseTimeout);
            utils.setTimeout(wordTimeout, t.wordTime);
        }
        utils.setTimeout(wordTimeout, this.wordTime);
    }

    if (this.mode == "self-paced reading") {    
        this.handleKey = function(code, time) {
            if (code == 32) {
                this.recordSprResult(time, this.currentWord);

                if (this.currentWord - 1 >= 0)
                    this.blankWord(this.currentWord - 1);
                if (this.currentWord < this.stoppingPoint)
                    this.showWord(this.currentWord);
                ++(this.currentWord);
                if (this.currentWord > this.stoppingPoint)
                    finishedCallback(this.resultsLines);

                return false;
            }
            else {
                return true;
            }
        };
    }

    this.recordSprResult = function(time, word) {
        if (word > 0 && word < this.stoppingPoint) {
            assert(this.previousTime, "Internal error in dashed_sentence.js");
            this.resultsLines.push([
                ["Word number", word],
                ["Word", this.words[word - 1]],
                ["Reading time", time - this.previousTime],
                ["Newline?", boolToInt((word > 0) && (this.wordDivs[word - 1].offsetTop !=
                                                      this.wordDivs[word].offsetTop))],
                ["Sentence MD5", this.sentenceMD5]
            ]);
        }
        this.previousTime = time;
    };
}

DashedSentence.htmlDescription = function (opts) {
    return document.createTextNode(opts.s);
}



/* This software is licensed under a BSD license; see the LICENSE file for details. */

FlashSentence.name = "FlashSentence";
FlashSentence.obligatory = ["s"];

function FlashSentence(div, options, finishedCallback, utils) {

    this.options = options;
    this.sentence = options.s;
    this.timeout = dget(options, "timeout", 2000);

    // Precalculate MD5 of sentence.
    var canonicalSentence = this.sentence.split('/\s/').join(' ');
    this.sentenceMD5 = hex_md5(canonicalSentence);

    this.div = div;
    this.div.className = "flashed-sentence";
    this.div.appendChild(document.createTextNode(this.sentence));

    if (this.timeout) {
        var t = this;
        utils.setTimeout(function() {
            finishedCallback([[["Sentence MD5", t.sentenceMD5]]]);
        }, this.timeout);
    }
    else {
        // Give results without actually finishing.
        if (utils.setResults)
            utils.setResults([[["Sentence MD5", this.sentenceMD5]]]);
    }
}

FlashSentence.htmlDescription = function (opts) {
    return document.createTextNode(opts.s);
}


/* This software is licensed under a BSD license; see the LICENSE file for details. */

Message.name = "Message";
Message.obligatory = ["html"];
Message.countsForProgressBar = false;

function Message(div, options, finishedCallback, utils) {
    this.div = div;
    this.options = options;
    this.hideProgressBar = dget(options, "hideProgressBar", true);

    this.html = options.html;
    div.className = "message";
    //div.innerHTML = this.html;
    div.appendChild(htmlCodeToDOM(this.html));

    // Bit of copy/pasting from 'Separator' here.
    this.transfer = dget(options, "transfer", "click");
    assert(this.transfer == "click" || this.transfer == "keypress" || typeof(this.transfer) == "number",
           "Value of 'transfer' option of Message must either be the string 'click' or a number");

    if (this.transfer == "click") {
        this.continueMessage = dget(options, "continueMessage", "Click here to continue.");
        this.consentRequired = dget(options, "consentRequired", false);
        this.consentMessage = dget(options, "consentMessage", "I have read the above and agree to do the experiment.");
        this.consentErrorMessage = dget(options, "consentErrorMessage", "You must consent before continuing.");

        // Add the consent checkbox if necessary.
        var checkbox = null;
        if (this.consentRequired) {
            var names = { };
            var dom = jsHTML(
                ["form",
                 [["table", {style: "border: none; padding: none; margin: none;"}],
                  ["tbody",
                   ["tr",
                    [["td", {style: "border: none; padding-left: 0; margin-left: 0;"}], [["input:checkbox", {type: "checkbox"}]]],
                    [["td:message", {style: "border: none; margin-left: 0; padding-left: 1em;"}], this.consentMessage]
                ]]]],
                names
            );
            checkbox = names.checkbox;
            this.div.appendChild(dom);
            // Allow clicking on the message as well as on the checkbox itself.
            names.message.onclick = function () {
                // This is more robust that names.checkbox.checked = ! names.checkbox.checked
                if (names.checkbox.checked)
                    names.checkbox.checked = false;
                else
                    names.checkbox.checked = true;
            }
            // Change cursor to pointer when hovering over the message (have to use JS because
            // IE doesn't support :hover for anything other than links).
            names.message.onmouseover = function () {
                names.message.style.cursor = "default";
            }
        }

        var t = this;
        // Get a proper lexical scope for the checkbox element so we can capture it in a closure.
        (function (checkbox) {
            var m = document.createElement("p");
            m.style.clear = "left";
            var a = document.createElement("a");
            a.href = "";
            a.className = "continue-link";
            a.onclick = function() {
                if ((! checkbox) || checkbox.checked)
                    finishedCallback();
                else
                    alert(t.consentErrorMessage); 
                return false;
            }
            a.appendChild(document.createTextNode("\u2192 " + t.continueMessage));
            m.appendChild(a);
            div.appendChild(m);
        })(checkbox);
    }
    else if (this.transfer == "keypress") {
        this.handleKey = function(code, time) {
            finishedCallback(null);
            return false;
        }
    }
    else {
        assert(! this.consentRequired, "The 'consentRequired' option of the Message controller can only be set to true if the 'transfer' option is set to 'click'.");
        utils.setTimeout(finishedCallback, this.transfer);
    }
}

Message.htmlDescription = function (opts) {
    var d = htmlCodeToDOM(opts.html);
    return truncateHTML(d, 100);
}


/* This software is licensed under a BSD license; see the LICENSE file for details. */

Question.name = "Question";
Question.obligatory = ["as"];

__Question_callback__ = null;
__Questions_answers__ = null;

function Question(div, options, finishedCallback, utils) {
    var questionField = "Question (NULL if none).";
    var answerField = "Answer";
    var correctField = "Whether or not answer was correct (NULL if N/A)";
    var timeField = "Time taken to answer.";

    this.div = div;
    this.options = options;

    div.className = "question";

    this.question = dget(options, "q");
    this.answers = options.as;

    this.hasCorrect = dget(options, "hasCorrect", false);
    // hasCorrect is either false, indicating that there is no correct answer,
    // true, indicating that the first answer is correct, or an integer giving
    // the index of the correct answer, OR a string giving the correct answer.
    // Now we change it to either false or an index.
    if (this.hasCorrect === true)
        this.hasCorrect = 0;
    if (typeof(this.hasCorrect) == "string") {
        var foundIt = false;
        for (var i = 0; i < this.answers.length; ++i) {
            if (this.answers[i].toLowerCase() == this.hasCorrect.toLowerCase()) {
                this.hasCorrect = i;
                foundIt = true;
                break;
            }
        }
        assert(foundIt, "Value of 'hasCorrect' option not recognized in Question");
    }
    this.showNumbers = dget(options, "showNumbers", true);
    this.presentAsScale = dget(options, "presentAsScale", false);
    this.randomOrder = dget(options, "randomOrder", ! (this.hasCorrect === false));
    this.timeout = dget(options, "timeout", null);
    this.instructions = dget(options, "instructions");
    this.leftComment = dget(options, "leftComment");
    this.rightComment = dget(options, "rightComment");

    if (! (this.hasCorrect === false))
        assert(typeof(this.hasCorrect) == "number" && this.hasCorrect < this.answers.length,
               "Bad index for correct answer in Question");

    if (this.randomOrder) {
        this.orderedAnswers = new Array(this.answers.length);
        for (var i = 0; i < this.answers.length; ++i)
            this.orderedAnswers[i] = this.answers[i];
        fisherYates(this.orderedAnswers);
    }
    else {
        this.orderedAnswers = this.answers;
    }

    this.setFlag = function(correct) {
        if (! correct) {
            utils.setValueForNextElement("failed", true);
        }
    }

    if (this.question) {
        this.qp = document.createElement("p");
        this.qp.className = "question-text";
        this.qp.appendChild(document.createTextNode(this.question));
    }
    this.xl = document.createElement(((! this.presentAsScale) && this.showNumbers) ? "ol" : "ul");
    this.xl.style.marginLeft = 0;
    this.xl.style.paddingLeft = 0;
    __Question_answers__ = new Array(this.answers.length);

    if (this.presentAsScale && this.leftComment) {
        var lcd = document.createElement("li");
        lcd.className = "scale-comment-box";
        lcd.appendChild(document.createTextNode(this.leftComment));
        this.xl.appendChild(lcd);
    }
    for (var i = 0; i < this.orderedAnswers.length; ++i) {
        var li;
        li = document.createElement("li");
        if (this.presentAsScale) {
            li.className = "scale-box";
            // IE doesn't support :hover for anything other than links, so we
            // have to use JS.
            (function (li) {
                li.onmouseover = function () {
                    li.style.borderColor = "black";
                    // With IE < 6, we have to use "hand" instead of "pointer".
                    var isOldIE = false;
                    /*@cc_on @if (@_jscript_version <= 5.5) isOldIE = true; @end @*/
                    li.style.cursor = (isOldIE ? "hand" : "pointer");
                };
                li.onmouseout = function () { li.style.borderColor = "#9ea4b1"; li.style.cursor = "default"; };
            })(li);
         }
        else {
            li.className = "normal-answer";
        }
        //li.onclick = new Function("__Question_callback__(" + i + ");");
        (function(i) {
            li.onclick = function () { __Question_callback__(i); };
        })(i);
        var ans = typeof(this.orderedAnswers[i]) == "string" ? this.orderedAnswers[i] : this.orderedAnswers[i][1];
        var t = this; // 'this' doesn't behave as a lexically scoped variable so can't be
                      // captured in the closure defined below.
        var a = document.createElement("span");
        a.className = "fake-link";
        //a.href = "javascript:__Question_callback__(" + i + ");";
        __Question_answers__[i] = ans;
        __Question_callback__ = function (i) {
            var answerTime = new Date().getTime();
            var ans = __Question_answers__[i];
            var correct = "NULL";
            if (! (t.hasCorrect === false)) {
                var correct_ans = typeof(t.answers[t.hasCorrect]) == "string" ? t.answers[t.hasCorrect] : t.answers[t.hasCorrect][1];
                correct = (ans == correct_ans ? 1 : 0);
                t.setFlag(correct);
            }
            finishedCallback([[[questionField, t.question ? url_encode_removing_commas(t.question) : "NULL"],
                               [answerField, url_encode_removing_commas(ans)],
                               [correctField, correct],
                               [timeField, answerTime - t.creationTime]]]);
        };
        a.appendChild(document.createTextNode(ans));
        li.appendChild(a);

        this.xl.appendChild(li);
    }
    if (this.presentAsScale && this.rightComment) {
        var rcd = document.createElement("li");
        rcd.className = "scale-comment-box";
        rcd.appendChild(document.createTextNode(this.rightComment));
        this.xl.appendChild(rcd);
    }

    if (! (this.qp === undefined))
        div.appendChild(this.qp);

    // Again, using tables to center because IE sucks.
    var table = document.createElement("table");
    if (conf_centerItems)
        table.align = "center";
    var tbody = document.createElement("tbody");
    var tr = document.createElement("tr");
    var td = document.createElement("td");
    if (conf_centerItems)
        td.align = "center";
    table.appendChild(tbody);
    tbody.appendChild(tr);
    tr.appendChild(td);
    td.appendChild(this.xl);
    div.appendChild(table);

    if (this.instructions) {
        var p = document.createElement("p");
        p.className = "instructions-text"
        if (conf_centerItems)
            p.style.textAlign = "center";
        p.appendChild(document.createTextNode(this.instructions));
        div.appendChild(p);
    }

    if (this.timeout) {
        var t = this;
        utils.setTimeout(function () {
            var answerTime = new Date().getTime();
            t.setFlag(false);
            finishedCallback([[[questionField, t.question ? url_encode_removing_commas(t.question) : "NULL"],
                               [answerField, "NULL"], [correctField, "NULL"],
                               [timeField, answerTime - t.creationTime]]]);
        }, this.timeout);
    }

    // TODO: A bit of code duplication in this function.
    var t = this;
    this.handleKey = function(code, time) {
        var answerTime = new Date().getTime();
        if ((! t.presentAsScale) && t.showNumbers &&
            ((code >= 48 && code <= 57) || (code >= 96 && code <= 105))) {
            // Convert numeric keypad codes to ordinary keypad codes.
            var n = code >= 96 ? code - 96 : code - 48;
            if (n > 0 && n <= t.orderedAnswers.length) {
                var ans = typeof(t.orderedAnswers[n-1]) == "string" ? t.orderedAnswers[n-1] : t.orderedAnswers[n-1][1];
                var correct = "NULL";
                if (! (t.hasCorrect === false)) {
                    var correct_ans = typeof(t.answers[t.hasCorrect]) == "string" ? t.answers[t.hasCorrect] : t.answers[t.hasCorrect][1];
                    correct = (correct_ans == ans ? 1 : 0);
                    t.setFlag(correct);
                }
                finishedCallback([[[questionField, t.question ? url_encode_removing_commas(t.question) : "NULL"],
                                   [answerField, url_encode_removing_commas(ans)],
                                   [correctField, correct],
                                   [timeField, answerTime = t.creationTime]]]);

                return false;
            }
            else {
                return true;
            }
        }
        // Letters (and numbers in the case of scales).
        else if ((code >= 65 && code <= 90) || (t.presentAsScale && ((code >= 48 && code <= 57) || (code >= 96 && code <= 105)))) {
            // Convert numeric keypad codes to ordinary keypad codes.
            code = (code >= 96 && code <= 105) ? code - 48 : code;
            for (var i = 0; i < t.answers.length; ++i) {
                var ans = null;
                if (typeof(t.answers[i]) == "string") {
                    if (code == t.answers[i].toUpperCase().charCodeAt(0))
                        ans = t.answers[i];
                }
                else {
                    if (code == t.answers[i][0].toUpperCase().charCodeAt(0))
                        ans = t.answers[i][1];
                }

                if (ans) {
                    var correct = "NULL";
                    if (! (t.hasCorrect === false)) {
                        var correct_ans = typeof(t.answers[t.hasCorrect]) == "string" ? t.answers[t.hasCorrect] : t.answers[t.hasCorrect][1];
                        correct = (correct_ans == ans ? 1 : 0);
                        t.setFlag(correct);
                    }
                    finishedCallback([[[questionField, t.question ? url_encode_removing_commas(t.question) : "NULL"],
                                       [answerField, url_encode_removing_commas(ans)],
                                       [correctField, correct],
                                       [timeField, answerTime - t.creationTime]]]);

                    return false;
                }
            }
        }

        return true;
    }

    // Store the time when this was first displayed.
    this.creationTime = new Date().getTime();
}

Question.htmlDescription = function(opts) {
    return document.createTextNode(opts.q);
}



/* This software is licensed under a BSD license; see the LICENSE file for details. */

Separator.name = "Separator";
Separator.obligatory = [];
Separator.countsForProgressBar = false;

function Separator(div, options, finishedCallback, utils) {
    this.div = div;
    this.options = options;

    this.ignoreFailure = dget(options, "ignoreFailure", false);
    this.style = this.ignoreFailure ? "normal" : (utils.getValueFromPreviousElement("failed") ? "error" : "normal");
    var x = utils.getValueFromPreviousElement("style");
    if (x) this.style = x;
    assert(this.style == "normal" || this.style == "error", "'style' property of Separator must either be 'normal' or 'error'");

    this.transfer = dget(options, "transfer", "keypress");
    assert(this.transfer == "keypress" || typeof(this.transfer) == "number",
           "Value of 'transfer' option of Separator must either be the string 'keypress' or a number");

    var normal_message = dget(options, "normalMessage", "Press any key to continue.");
    var x = utils.getValueFromPreviousElement("normalMessage");
    if (x) normal_message = x;

    var error_message = dget(options, "errorMessage", "Wrong. Press any key to continue.");
    var x = utils.getValueFromPreviousElement("errorMessage");
    if (x) error_message = x;

    var p = document.createElement("p");
    div.appendChild(p);
    if (this.style == "error") {
        div.className = "next-item-failure-message";
        p.appendChild(document.createTextNode(error_message));
    }
    else {
        div.className = "next-item-message";
        p.appendChild(document.createTextNode(normal_message));
    }

    if (this.transfer == "keypress") {
        this.handleKey = function(code, time) {
            finishedCallback(null);
            return false;
        }
    }
    else {
        utils.setTimeout(function () {
            finishedCallback(null);
        }, this.transfer);
    }
}

Separator.htmlDescription = function (opts) {
    return document.createTextNode(opts.normalMessage);
}



/* This software is licensed under a BSD license; see the LICENSE file for details. */

VBox.name = "VBox";
VBox.obligatory = ["children", "triggers"]

function VBox(div, options, finishedCallback, utils) {
    this.options = options;
    this.children = options.children;
    this.triggers = options.triggers;
    this.padding = dget(options, "padding", "2em");

    assert_is_arraylike(this.children, "The 'children' option of VBox must be an array");
    assert(this.children.length % 2 == 0, "The 'children' array for VBox must contain an even number of elements");

    assert_is_arraylike(this.triggers, "The 'triggers' option of VBox must be an array");
    assert(this.triggers.length > 0, "The 'triggers' array for VBox must be an array of length > 0");

    var t = this;
    iter(this.triggers, function (tr) {
        assert(typeof(tr) == "number", "The 'triggers' array for VBox must be an array of integers");
        assert(tr >= 0 && tr < t.children.length / 2,
               "Numbers in the 'triggers' array must be indices into the 'children' array starting from 0");
    });

    this.indicesAndResultsOfThingsThatHaveFinished = [];
    this.childInstances = [];
    this.childUtils = [];

    for (var i = 0; i < this.children.length; i += 2) {
        var controllerClass = this.children[i];
        var childOptions = this.children[i + 1];
        childOptions = merge_dicts(get_defaults_for(controllerClass), childOptions);

        var d = document.createElement("p");
        d.style.clear = "both";

        // Call a manipulator if one was supplied.
        if (! (options.manipulators === undefined)) {
            for (var j = 0; j < options.manipulators.length; ++j) {
                if (options.manipulators[j][0] == (i / 2))
                    d = options.manipulators[j][1](d);
            }
        }

        // Add padding if requested.
        var dd = null;
        if (this.padding && i > 0) {
            dd = document.createElement("div");
            dd.style.marginTop = this.padding;
            dd.style.marginBottom = 0;
            dd.appendChild(d);
        }

        // Wrap in a table if we're centering things.
        var ddd = null;
        if (conf_centerItems) {
            ddd = document.createElement("table");
            ddd.align = "center";
            var tb = document.createElement("tbody");
            var tr = document.createElement("tr");
            var td = document.createElement("td");
            ddd.appendChild(tb);
            tb.appendChild(tr);
            tr.appendChild(td);
            td.appendChild(dd ? dd : d);
        }

        // Add the actual child.
        div.appendChild(ddd ? ddd : (dd ? dd : d));    

        var u = new Utils(utils.getValuesFromPreviousElement());
        this.childUtils.push(u);
        (function(i) {
            u.setResults = function(results) {
                t.indicesAndResultsOfThingsThatHaveFinished.push([i, results]);
            };
        })(i);

        var l = this.childUtils.length - 1;
        // Get around JavaScript's silly closure capture behavior (deriving
        // from weird variable scoping rules).
        // See http://calculist.blogspot.com/2005/12/gotcha-gotcha.html
        (function(l) {
            t.childInstances.push(
                new controllerClass(
                    d,
                    childOptions,
                    function (r) { t.myFinishedCallback(l, r); },
                    u
                )
            );
        })(l);
    }

    this.handleKey = function(code, time) {
        iter(this.childInstances, function (c) {
            if (c.handleKey)
                c.handleKey(code, time);
        });
    }

    this.myFinishedCallback = function(index, results) {
        this.childUtils[index].gc();
        this.indicesAndResultsOfThingsThatHaveFinished.push([index, results]);

        var satisfied = true;
        for (var i = 0; i < this.triggers.length; ++i) {
            var foundIt = false;
            for (var j = 0; j < this.indicesAndResultsOfThingsThatHaveFinished.length; ++j) {
                if (this.indicesAndResultsOfThingsThatHaveFinished[j][0] == this.triggers[i]) {
                    foundIt = true;
                    break;
                }
            }
            if (! foundIt) {
                satisfied = false;
                break;
            }
        }

        if (satisfied) {
            // Merge values for next element.
            var merged = merge_list_of_dicts(map(function (x) { return x.valuesForNextElement; },
                                                 this.childUtils));
            utils.valuesForNextElement = merged;

            finishedCallback(this.concatResults(this.indicesAndResultsOfThingsThatHaveFinished));
        }
    }

    this.concatResults = function(iar) {
        iar = iar.sort(function(x, y) { return x[0] - y[0]; });
        var res = [];
        for (var i = 0; i < iar.length; ++i) {
            for (var j = 0; j < iar[i][1].length; ++j) {
                var line = [];
                for (var k = 0; k < iar[i][1][j].length; ++k)
                    line.push(iar[i][1][j][k]);
                res.push(line);
            }
        }
        return res;
    }
}


