(function (jsinput, undefined) {
    // Initialize js inputs on current page.
    // N.B.: No library assumptions about the iframe can be made (including,
    // most relevantly, jquery). Keep in mind what happens in which context
    // when modifying this file.

    /*      Check whether there is anything to be done      */

    // When all the problems are first loaded, we want to make sure the
    // constructor only runs once for each iframe; but we also want to make
    // sure that if part of the page is reloaded (e.g., a problem is
    // submitted), the constructor is called again.

    if (!jsinput) {
        jsinput = {
            runs : 1,
            arr : [],
            exists : function(id) {
                jsinput.arr.filter(function(e, i, a) {
                    return e.id = id;
                });
            }
        };
    }

    jsinput.runs++;


    /*                      Utils                               */


    // Take a string and find the nested object that corresponds to it. E.g.:
    //    deepKey(obj, "an.example") -> obj["an"]["example"]
    var _deepKey = function(obj, path){
        for (var i = 0, p=path.split('.'), len = p.length; i < len; i++){
            obj = obj[p[i]];
        }
        return obj;
    };


    /*      END     Utils                                   */




    function jsinputConstructor(spec) {
        // Define an class that will be instantiated for each jsinput element
        // of the DOM

        // 'that' is the object returned by the constructor. It has a single
        // public method, "update", which updates the hidden input field.
        var that = {};

        /*                      Private methods                          */

        var sect = $(spec.elem).parent().find('section[class="jsinput"]');
        var sectAttr = function (e) { return $(sect).attr(e); };
        var thisIFrame = $(spec.elem).
                        find('iframe[name^="iframe_"]').
                        get(0);
        var cWindow = thisIFrame.contentWindow;

        // Get the hidden input field to pass to customresponse
        function _inputField() {
            var parent = $(spec.elem).parent();
            return parent.find('input[id^="input_"]');
        }
        var inputField = _inputField();

        // Get the grade function name
        var getGradeFn = sectAttr("data");
        // Get state getter
        var getStateGetter = sectAttr("data-getstate");
        // Get state setter
        var getStateSetter = sectAttr("data-setstate");
        // Get stored state
        var getStoredState = sectAttr("data-stored");



        // Put the return value of gradeFn in the hidden inputField.
        var update = function () {
            var ans;

            ans = _deepKey(cWindow, gradeFn)();
            // setstate presumes getstate, so don't getstate unless setstate is
            // defined.
            if (getStateGetter && getStateSetter) {
                var state, store;
                state = unescape(_deepKey(cWindow, getStateGetter)());
                store = {
                    answer: ans,
                    state:  state
                };
                inputField.val(JSON.stringify(store));
            } else {
                inputField.val(ans);
            }
            return;
        };

        /*                       Public methods                     */

        that.update = update;



        /*                      Initialization                          */

        jsinput.arr.push(that);

        // Put the update function as the value of the inputField's "waitfor"
        // attribute so that it is called when the check button is clicked.
        function bindCheck() {
            inputField.data('waitfor', that.update);
            return;
        }

        var gradeFn = getGradeFn;


        bindCheck();

        // Check whether application takes in state and there is a saved
        // state to give it. If getStateSetter is specified but calling it
        // fails, wait and try again, since the iframe might still be
        // loading.
        if (getStateSetter && getStoredState) {
            var sval, jsonVal;

            try {
              jsonVal = JSON.parse(getStoredState);
            } catch (err) {
              jsonVal = getStoredState;
            }

            if (typeof(jsonVal) === "object") {
                sval = jsonVal["state"];
            } else {
                sval = jsonVal;
            }


            // Try calling setstate every 200ms while it throws an exception,
            // up to five times; give up after that.
            // (Functions in the iframe may not be ready when we first try
            // calling it, but might just need more time. Give the functions
            // more time.)
            function whileloop(n) {
                if (n < 5){
                    try {
                        _deepKey(cWindow, getStateSetter)(sval);
                    } catch (err) {
                        setTimeout(whileloop(n+1), 200);
                    }
                }
                else {
                    console.debug("Error: could not set state");
                }
            }
            whileloop(0);

        }


        return that;
    }


    function walkDOM() {
        var newid;

        // Find all jsinput elements, and create a jsinput object for each one
        var all = $(document).find('section[class="jsinput"]');

        all.each(function(index, value) {
            // Get just the mako variable 'id' from the id attribute
            newid = $(value).attr("id").replace(/^inputtype_/, "");


            if (!jsinput.exists(newid)){
                var newJsElem = jsinputConstructor({
                    id: newid,
                    elem: value,
                });
            }
        });
    }

    // This is ugly, but without a timeout pages with multiple/heavy jsinputs
    // don't load properly.
    if ($.isReady) {
        setTimeout(walkDOM, 300);
    } else {
        $(document).ready(setTimeout(walkDOM, 300));
    }

})(window.jsinput = window.jsinput || false);