//polyfill for Object.assign(). Taken from MDN.
if (typeof Object.assign != 'function') {
	Object.assign = function(target) {
		// We must check against these specific cases.
		if (target === undefined || target === null) {
			throw new TypeError('Cannot convert undefined or null to object');
		}
		var output = Object(target);
		for (var index = 1; index<arguments.length; index++) {
			var source = arguments[index];
			if (source !== undefined && source !== null) {
				for (var nextKey in source) {
					if (source.hasOwnProperty(nextKey)) {
						output[nextKey] = source[nextKey];
					}
				}
			}
		}
		return output;
	};
}

//polyfill for CustomEvent(). Taken from MDN.
if (typeof window.CustomEvent !== 'function') {
	CustomEvent = function(event, params) {
		params = params || {
				bubbles: false,
				cancelable: false,
				detail: undefined
			};
		var evt = document.createEvent('CustomEvent');
		evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail);
		return evt;
	};

	CustomEvent.prototype = window.Event.prototype;
	window.CustomEvent = CustomEvent;
}

//partial polyfill for .bind(). Taken from MDN.
if (!Function.prototype.bind) {
	Function.prototype.bind = function (oThis) {
		if (typeof this !== 'function') {
			// closest thing possible to the ECMAScript 5
			// internal IsCallable function
			throw new TypeError('Function.prototype.bind - what is trying to be bound is not callable');
		}
		var aArgs = Array.prototype.slice.call(arguments, 1),
			fToBind = this,
			fNOP = function () {
			},
			fBound = function () {
				return fToBind.apply(this instanceof fNOP ? this : oThis,
					aArgs.concat(Array.prototype.slice.call(arguments)));
			};
		if (this.prototype) {
			// Function.prototype don't have a prototype property
			fNOP.prototype = this.prototype;
		}
		fBound.prototype = new fNOP();
		return fBound;
	};
}

// http://paulirish.com/2011/requestanimationframe-for-smart-animating/
// http://my.opera.com/emoller/blog/2011/12/20/requestanimationframe-for-smart-er-animating
// requestAnimationFrame polyfill by Erik Möller. fixes from Paul Irish and Tino Zijdel
// MIT license
(function () {
	var lastTime = 0;
	var vendors = ['ms', 'moz', 'webkit', 'o'];
	for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
		window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
		window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] ||
			window[vendors[x] + 'CancelRequestAnimationFrame'];
	}
	if (!window.requestAnimationFrame) {
		window.requestAnimationFrame = function (callback, element) {
			var currTime = new Date().getTime();
			var timeToCall = Math.max(0, 16 - (currTime - lastTime));
			var id = window.setTimeout(function () {
					callback(currTime + timeToCall);
				},
				timeToCall);
			lastTime = currTime + timeToCall;
			return id;
		};
	}
	if (!window.cancelAnimationFrame) {
		window.cancelAnimationFrame = function (id) {
			clearTimeout(id);
		};
	}
}());

if (typeof window.console == 'undefined') {
	window.console = {
		log: function () {
		}
	};
}

if (!String.prototype.trim) {
	String.prototype.trim = function () {
		return this.replace(/^[\s\uFEFF\xA0]+|[\s\uFEFF\xA0]+$/g, '');
	};
}

if (!Array.isArray) {
	Array.isArray = function (arg) {
		return Object.prototype.toString.call(arg) === '[object Array]';
	};
}

if (!Array.prototype.indexOf) {
	Array.prototype.indexOf = function (obj, start) {
		for (var i = (start || 0), j = this.length; i<j; i++) {
			if (this[i] === obj) {
				return i;
			}
		}
		return -1;
	};
}

(function() {
    if (!Event.prototype.preventDefault) {
        Event.prototype.preventDefault = function () {
            this.returnValue = false;
        };
    }

    if (!Event.prototype.stopPropagation) {
        Event.prototype.stopPropagation = function () {
            this.cancelBubble = true;
        };
    }

    if (!Element.prototype.addEventListener) {
        var eventListeners = [];
        var addEventListener = function (type, listener /*, useCapture (will be ignored) */) {
            var self = this;
            var wrapper = function (e) {
                e.which = e.keyCode || e.button;
                e.target = e.srcElement;
                e.currentTarget = self;
                if (listener.handleEvent) {
                    listener.handleEvent(e);
                }
                else {
                    listener.call(self, e);
                }
            };
            if (type == 'DOMContentLoaded') {
                var wrapper2 = function (e) {
                    if (document.readyState == 'complete') {
                        wrapper(e);
                    }
                };
                document.attachEvent('onreadystatechange', wrapper2);
                eventListeners.push({
                    object: this,
                    type: type,
                    listener: listener,
                    wrapper: wrapper2
                });
                if (document.readyState == 'complete') {
                    var e = new Event();
                    e.srcElement = window;
                    wrapper2(e);
                }
            }
            else {
                this.attachEvent('on' + type, wrapper);
                eventListeners.push({
                    object: this,
                    type: type,
                    listener: listener,
                    wrapper: wrapper
                });
            }
        };
        var removeEventListener = function (type, listener /*, useCapture (will be ignored) */) {
            var counter = 0;
            while (counter < eventListeners.length) {
                var eventListener = eventListeners[counter];
                if (eventListener.object == this && eventListener.type == type && eventListener.listener == listener) {
                    if (type == 'DOMContentLoaded') {
                        this.detachEvent('onreadystatechange', eventListener.wrapper);
                    }
                    else {
                        this.detachEvent('on' + type, eventListener.wrapper);
                    }
                    eventListeners.splice(counter, 1);
                    break;
                }
                ++counter;
            }
        };
        Element.prototype.addEventListener = addEventListener;
        Element.prototype.removeEventListener = removeEventListener;
        if (HTMLDocument) {
            HTMLDocument.prototype.addEventListener = addEventListener;
            HTMLDocument.prototype.removeEventListener = removeEventListener;
        }
        if (Window) {
            Window.prototype.addEventListener = addEventListener;
            Window.prototype.removeEventListener = removeEventListener;
        }
    }
})();
define("modules/polyfill", function(){});

(function() {
	"use strict";
	require.config(
		{
			baseUrl: 'scripts',
			deps: [
                //'libs/vle/vleapi.1',
				'modules/polyfill'
			],
			paths: {
				/* modules */
				"buttons": "modules/buttons",
				"input": "modules/input",
                "inline": "modules/inline",
                "inline-data": "../data/inline/inline_data",
				"saver": "modules/saver",
				"table": "modules/table",
				"table-input": "modules/table-input",
				"table-extend": "modules/table-extend",
				"table-save": "modules/table-save",
				"table-data": "../data/table/table_data",
				"table-config": "modules/table_config",
				"utils": "modules/utils",
				"feedback": "modules/feedback",
                "dnd": "modules/dnd",
                "dnd-choices": "modules/dnd-choices",
                "dnd-targets": "modules/dnd-targets",
                "dnd-data": "../data/dnd/dnd_data",
                "loader": "modules/loader",
                "spinner": "modules/spinner",
                /*3rd party*/
                'jquery': 'libs/jquery/jquery.min',
                'jquery_ui': 'libs/jquery/jquery-ui.min',
                'touch_punch': 'libs/jquery/jquery.ui.touch-punch.min',
                'vle': '../vleapi.1'
            },
            shim: {
                'jquery_ui': {
                    deps: ['jquery']
                },
                'touch_punch': {
                    deps: ['jquery']
                },
                'vle': {
                    exports: 'VLE'
                }
            }
		});
})();
define("main", function(){});

define('utils',{
    svgNS: 'http://www.w3.org/2000/svg',
    svgXLinkNS: 'http://www.w3.org/1999/xlink',

    detectProtocol: (function() {
        var protocol = location.protocol;
        var el = document.getElementById('container');

        if (protocol !== "http:" && protocol !== "https:") {
            if (el) {
                el.className = 'not-http';
                return true;
            }
        }

        return false;
    })(),

	escapeRegExp: function(str) {
		return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, '\\$&');
	},

    setBoxDimension: function(el, rule, value) {
        el.style[rule] = value + ((typeof value == 'number') ? 'px' : '');
    },

	createElement: function(type, classes, attr) {
        var el = document.createElement(type);

        if (typeof classes != 'undefined') {
            el.className = (Array.isArray(classes)) ? classes.join(' ') : classes.toString();
        }

        switch (typeof attr) {
            case 'string':
                el.id = attr;
                break;
            case 'object':
                for (var p in attr) {
                    if (attr.hasOwnProperty(p)) {
                        el.setAttribute(p, attr[p]);
                    }
                }
        }

        return el;
    },

    resize_iframe: function(height) {
        var html, styles;

        if (!!window.frameElement) {
            html = document.getElementsByTagName('html')[0];
            styles = getComputedStyle(html);

            window.frameElement.height = (height || parseFloat(styles.marginTop) + parseFloat(styles.marginBottom) + html.offsetHeight);
        }
    },

    triggerCustomEvent: function(el, type, detail, bubbles) {
        console.log('triggered: ' + type);

        el.dispatchEvent(new CustomEvent(type, {
            detail: detail || {},
            bubbles: (bubbles !== false),
            cancelable: true
        }));
    },

    getQueryStringAsObj: function() {
        var source = window.location.search;
        var obj = {};
        var groups, tmp, i;

        if (source === '') {
            return false;
        }

        groups = source.substr(1).split('&');

        groups.forEach(function(group) {
            tmp = group.split('=');
            obj[decodeURIComponent(tmp[0])] = decodeURIComponent(tmp[1]);
        });

        return obj;
    },

    getAttributes: function(el) {
        var attrs, i;
        var obj = {};

        if (el.hasAttributes()) {
            attrs = el.attributes;
            for (i = attrs.length - 1; i >= 0; i--) {
                obj[attrs[i].name] = attrs[i].value;
            }
        } else {
            return false;
        }

        return obj;
    },

    toArray: function(arrLike) { // or asArray(), or array(), or *whatever*
        return [].slice.call(arrLike);
    },

    spliceArray: function(array, index, arrayToInsert) {
        Array.prototype.splice.apply(array, [index, 0].concat(arrayToInsert));
    },

    invertArray: function(arr) {
        var i, len;
        var inverted = [];

        for (i = 0, len = arr.length; i < len; i++) {
            if (Array.isArray(arr[i])) {
                inverted[i] = this.invertArray(arr[i]);
            }
            else {
                inverted[i] = (arr[i] <= 0) ? Math.abs(arr[i]) : -Math.abs(arr[i]);
            }
        }

        return inverted;
    },

    closest: function(array, val) {
        return array.reduce(function(prev, curr) {
            return (Math.abs(curr - val) < Math.abs(prev - val) ? curr : prev);
        });
    },

    /**
     * find the nearest lower value than num in array
     * @param array
     * @param num
     * @returns {number}
     */
    nextLowest: function(array, num) {
        var closest = Math.min.apply(null, array);

        array.forEach(function(val) {
            closest = (val <= num && val > closest) ? val : closest;
        });

        return closest;
    },

    getPopulatedArray: function(num, val) {
        var arr = [], i;

        for (i = 0; i < num; i++) {
            if (this.isObject(val)) {
                //make if a new one if so...
                val = Array.isArray(val) ? [] : {};
            }

            arr.push(val);
        }

        return arr;
    },

    setCfg: function(target, source) {
        var i;

        target = target || {};

        for (i in source) {
            if (source.hasOwnProperty(i)) {
                if (typeof source[i] == 'object') {
                    target[i] = target[i] || (Array.isArray(source[i]) ? [] : {});
                    this.setCfg(target[i], source[i]);
                }
                else {
                    if (Array.isArray(target) || target.hasOwnProperty(i)) {
                        target[i] = source[i];
                    }
                }
            }
        }
    },

    extendDeep: function(target, source) {
        var i;

        target = target || {};

        for (i in source) {
            if (source.hasOwnProperty(i)) {
                if (typeof source[i] == 'object' && !(source[i] instanceof Element)) {
                    target[i] = target[i] || (Array.isArray(source[i]) ? [] : {});
                    this.extendDeep(target[i], source[i]);
                }
                else {
                    target[i] = source[i];
                }
            }
        }
    },

    setStyles: function(el, styles) {
        var p;

        for (p in styles) {
            if (styles.hasOwnProperty(p)) {
                switch (p) {
                    case 'backgroundImage':
                        el.style[p] = 'url(' + styles[p] + ')';
                        el.style.backgroundRepeat = 'no-repeat';
                        break;
                    case 'width':
                    case 'height':
                    case 'marginLeft':
                    case 'marginRight':
                    case 'marginTop':
                    case 'marginBottom':
                        if (!isNaN(parseFloat(styles[p])) && isFinite(styles[p])) {
                            el.style[p] = styles[p] + 'px';
                        }
                        break;
                    default:
                        el.style[p] = styles[p];
                }
            }
        }
    },

    removeFalseyValues: function(array) {
        if (Array.isArray(array)) {
            return array.reduce(function(prev, curr) {
                return prev + (curr ? ' ' + curr : '');
            }, '');
        }
    },

    //use get/setAttribute for class related functions for SVG
    hasClass: function(el, selector) {
        var className = el.getAttribute('class');

        if (className) {
            return className.split(' ').indexOf(selector) > -1;
        }

        return false;
    },

    addClass: function(el, selector) {
        if (this.hasClass(el, selector)) {
            return;
        }

        var className = el.getAttribute('class') || '';

        el.setAttribute('class', className += (className === '') ? selector : ' ' + selector);
    },

    removeClass: function(el, selector) {
        var className = el.getAttribute('class');
        var array, index;

        if (className) {
            array = className.split(' ');
            index = array.indexOf(selector);

            if (index > -1) {
                if (array.length == 1) {
                    el.removeAttribute('class');
                }
                else {
                    array.splice(index, 1);
                    el.setAttribute('class', array.join(' '));
                }
            }
        }
    },

    //TODO can we keep track of new order
    shuffle: function(a) {
        var j, x, i;

        for (i = a.length; i; i--) {
            j = Math.floor(Math.random() * i);
            x = a[i - 1];
            a[i - 1] = a[j];
            a[j] = x;
        }
    },

    forIn: function(data, callback) {
        var k, i = 0;

        for (k in data) {
            if (data.hasOwnProperty(k)) {
                callback(data[k], k, i++);
            }
        }
    },

    getResetObj: function(obj) {
        var values = {};

        for (var k in obj) {
            if (obj.hasOwnProperty(k)) {
                values[k] = '';
            }
        }

        return values;
    },

    getOffset: function(el) {
        var test = el, top = 0, left = 0;

        while (!!test && test.tagName.toLowerCase() !== 'body') {
            top += test.offsetTop;
            left += test.offsetLeft;
            test = test.offsetParent;
        }

        return {
            top: top,
            left: left
        };
    },

    /**
     * @param selector {(string|object)} CSS selector, DOM element or jQuery object
     * @param degree {(number|string)} degrees to rotate element
     */
    rotate: function(selector, degree) {
        $(selector).css({
            '-ms-transform': 'rotate(' + degree + 'deg)',
            //'-ms-transform-origin': '50% 50%',
            '-webkit-transform': 'rotate(' + degree + 'deg)',
            //'-webkit-transform-origin': '50% 50%',
            'transform': 'rotate(' + degree + 'deg)'
            //'transform-origin': '50% 50%'
        });
    },

    /**
     * @param svg {object} SVG DOM element
     * @param args {object} arguments object
     */
    improveSVGAccessibility: function(svg, args) {
        var title = document.createElementNS('http://www.w3.org/2000/svg', 'title');
        var description = document.createElementNS('http://www.w3.org/2000/svg', 'desc');

        if (svg.nodeName.toLowerCase() != 'svg') {
            console.log('No SVG element passed');
            return;
        }

        if (args) {
            title.textContent = (!!args.title) ? args.title : 'Interactive diagram';
            description.textContent = (!!args.description) ? args.description : 'An interactive diagram with clickable areas';
        }

        svg.setAttribute('aria-labelledby', 'title desc'); //aria-describedby should be used when better supported
        svg.insertBefore(description, svg.firstChild);
        svg.insertBefore(title, svg.firstChild);
    },

    /**
     * @param selector {string} CSS selector
     * @param context {object} SVG element
     * @param toggle boolean
     */
    makeSVGElementTabbable: function(selector, context, toggle) {
        var elements = Array.prototype.slice.call(context.querySelectorAll(selector));
        var a, i, attrs, ref, array = [];

        elements.forEach(function(el) {
            a = document.createElementNS('http://www.w3.org/2000/svg', 'a');
            ref = el.previousSibling;

            a.setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', 'javascript:void(0);');
            a.setAttribute('role', 'button');

            if (toggle) {
                a.setAttribute('aria-pressed', 'false');
            }

            if (el.hasAttributes()) {
                attrs = el.attributes;

                for (i = attrs.length - 1; i >= 0; i--) {
                    a.setAttribute(attrs[i].name, attrs[i].value);
                }
            }

            while (el.lastChild) {
                a.appendChild(el.firstChild);
            }

            array.push(a);
            el.parentNode.replaceChild(a, el);
        });

        return array;
    },

    /**
     * @param selector {string} CSS selector
     * @param context {object} an ancestor DOM element to use as context for selector
     */
    reverseChildren: function(selector, context) {
        context = (typeof context === 'undefined') ? document : context;
        var children = context.querySelectorAll(selector);
        var parent = children[0].parentNode;
        var i;

        if (parent.nodeName.toLowerCase() == 'svg') {
            parent = parent.insertBefore(document.createElementNS(this.svgNS, 'g'), children[0]);

            for (i = 0; i < children.length; i++) {
                parent.appendChild(children[i]);
            }
        }

        i = parent.childNodes.length;

        while (i--) {
            parent.appendChild(parent.childNodes[i]);
        }
    },

    isCanvasSupported: function() {
        var elem = document.createElement('canvas');
        return !!(elem.getContext && elem.getContext('2d'));
    },

    isSVGSupported: function() {
        return document.implementation.hasFeature("http://www.w3.org/TR/SVG11/feature#BasicStructure", "1.1");
    },

    showUnsupportedMsg: function(el, msg) {
        var msgDiv = document.createElement('div');

        el = el || document.getElementsByTagName('body')[0];
        msg = msg || 'Your browser does not support certain technologies used in this activity. To view the activity, please reopen it in a modern browser (e.g. IE9+, Chrome or Firefox).';

        msgDiv.className = 'unsupported-msg';
        msgDiv.innerHTML = msg;

        console.log(el, msgDiv);

        el.appendChild(msgDiv);
    },

    toCamelCase: function(str) {
        return str.replace(/[-_]([a-z])/g, function(m, w) {
            return w.toUpperCase();
        });
    },

    capFirstLetter: function(string) {
        return string.charAt(0).toUpperCase() + string.slice(1);
    },

    inWords: function(num) {
        var a = ['', 'one ', 'two ', 'three ', 'four ', 'five ', 'six ', 'seven ', 'eight ', 'nine ', 'ten ', 'eleven ', 'twelve ', 'thirteen ', 'fourteen ', 'fifteen ', 'sixteen ', 'seventeen ', 'eighteen ', 'nineteen '];
        var b = ['', '', 'twenty', 'thirty', 'forty', 'fifty', 'sixty', 'seventy', 'eighty', 'ninety'];
        var c = ['thousand', 'million', ''];
        var words = '';

        num = num.toString();

        if (num.length > 9) {
            return ''; // Number is larger than what function can deal with
        }

        //num = ('000000000' + num).substr(-9); // Make number into a predictable nine character string
        num = ('000000000' + num).slice(-9); // Make number into a predictable nine character string
        num = num.match(/.{3}/g); // Split string into chunks of three numbers then reverse order of returned array

        for (var i = 0; i < c.length; i++) {
            var n = num[i];
            var str = '';

            str += (words !== '') ? ' ' + c[i] + ' ' : '';
            str += (parseInt(n[0]) !== 0) ? (a[Number(n[0])] + 'hundred ') : '';
            n = n.substr(1);
            str += (parseInt(n) !== 0) ? (( str !== '' ) ? 'and ' : '') + (a[Number(n)] || b[n[0]] + ' ' + a[n[1]]) : '';
            words += str;
        }

        return words;
    },

    isObject: function(obj) {
        return obj === Object(obj);
    },

    isPlainObject: function(obj) {
        return typeof obj == 'object' && obj.constructor == Object;
    },

    /**
     *
     * @param {string} placeholder
     * @param {string} str
     * @param {string} replacement
     */
    replacePlaceholders: function (placeholder, str, replacement) {

    }
});
define(
    'buttons',['utils'],
    function(utils) {
        "use strict";

        var create = function(data) {
            if (Array.isArray(data)) {
                return createGroup(data);
            }
            else {
                return createSingle(data);
            }
        };

        /**
         * @param {object[]} data
         * @returns {Element|*}
         */
        var createGroup = function(data) {
            var group = OU.utils.createElement('div', 'btn-group');

            data.forEach(function(data) {
                group.appendChild(createSingle(data));
            });

            return group;
        };

        /**
         * @param {object} data
         * @param {string} data.type
         * @param {string|object[]} [data.html]
         * @param {boolean} [data.hidden]
         * @param {boolean} [data.disabled]
         * @param {object[]} [data.classes]
         * @param {string} [data.name]
         * @param {string} [data.value]
         * @param {string} [data.label]
         * @param {boolean} [data.toggle]
         * @param {function} [data.click]
         * @param {boolean} [construct]
         * @returns {Element|*}
         */
        var createSingle = function(data, construct) {
            var btn = document.createElement('button');
            var classes = [
                'btn',
                'btn-' + data.type.replace(/_/g, '-').toLowerCase()
            ];

            if (data.html) {
                btn.insertAdjacentHTML('beforeend', Array.isArray(data.html) ? data.html[0] : data.html);
            }
            if (data.name) {
                btn.name = data.name;
            }
            if (data.value) {
                btn.value = data.value;
            }
            if (Array.isArray(data.classes)) {
                classes = classes.concat(data.classes);
            }
            if (data.hidden) {
                btn.style.display = 'none';
            }
            if (data.disabled) {
                btn.disabled = true;
            }
            if (data.label) {
                btn.setAttribute('aria-label', data.label);
            }
            if (data.toggle) {
                btn.setAttribute('aria-pressed', 'false');
            }
            if (!(!!construct)) {
                if (data.click) {
                    btn.addEventListener('click', data.click);
                }
                else {
                    btn.addEventListener('click', utils.triggerCustomEvent.bind(null, btn, data.type));
                }
            }

            btn.setAttribute('type', 'button');
            btn.className = classes.join(' ');

            return btn;
        };

        /**
         * @param {object} data
         * @returns {ButtonGroup|Button}
         */
        var construct = function(data) {
            if (!data.type) {
                return new ButtonGroup(data);
            }
            else {
                return new Button(data);
            }
        };

        /**
         * @param {object} cfg
         * @param {object} [cfg.data] - extendable/overwritable object (can't be an array as unique keys needed)
         * @param {object[]} [cfg.enabled] - list of buttons to enable
         * @constructor
         */
        function ButtonGroup(cfg) {
            ButtonGroup.counter = ++ButtonGroup.counter || 0;

            this.id = 'btn-group' + ButtonGroup.counter;
            this.msg = null;
            this.data = {
                check: {
                    html: 'Check your answer'
                },
                reveal: {
                    html: ['Reveal answer', 'Hide answer']
                },
                save: {
                    html: 'Save'
                },
                reset: {
                    html: 'Reset'
                },
                retry: {
                    html: 'Try again'
                }
            };
            this.enabled = ['reset'];
            this.buttons = [];

            this.init(cfg);
        }

        ButtonGroup.prototype = {
            init: function (cfg) {
                console.log(cfg);

                utils.extendDeep(this, cfg);

                this.group = document.createElement('div');
                this.group.className = 'btn-group';

                if (this.msg) {
                    this.msg = document.createElement('div');
                    this.msg.className = 'btn-msg';
                }

                if (this.enabled == '*') {
                    this.enabled = Object.keys(this.data);
                }

                this.enabled.forEach(function (type) {
                    if (this.data[type]) {
                        this.data[type].type = type;
                        this.data[type].group = this.id;
                        this.buttons[type] = construct(this.data[type]);
                        this.buttons.push(this.buttons[type]);

                        if (type !== 'retry') {
                            this.group.appendChild(this.buttons[type].el);
                        }
                    }
                }.bind(this));

                this.setControllers();
            },

            active: function (type) {
                return !!this.buttons[type];
            },

            get: function (type) {
                return this.active(type) ? this.buttons[type] : false;
            },

            check: function () {
                if (this.buttons.retry) {
                    this.disable('check');
                }
            },

            reveal: function () {
                this.disable('check');

                if (!this.buttons.reveal.toggleable) {
                    this.disable('reveal');
                }
            },

            reset: function () {
                this.enable();

                if (this.buttons.reveal && this.buttons.reveal.pressed) {
                    this.buttons.reveal.toggle();
                }
            },

            retry: function () {
                this.enable();
            },

            enable: function (type) {
                if (type) {
                    if (this.buttons[type]) {
                        this.buttons[type].enable();
                    }
                }
                else {
                    this.buttons.forEach(function (btn) {
                        btn.enable();
                    });
                }
            },

            disable: function (type) {
                if (type) {
                    if (this.buttons[type]) {
                        this.buttons[type].disable();
                    }
                }
                else {
                    this.buttons.forEach(function (btn) {
                        btn.disable();
                    });
                }
            },

            setControllers: function () {
                this.buttons.forEach(function (btn) {
                    //TODO is it worth handling events internally via subscribe/publish?
                    document.body.addEventListener(btn.type, this);
                }.bind(this));
            },

            handleEvent: function (e) {
                if (e.detail.group != this.id) {
                    return;
                }

                if (typeof this[e.type] == 'function') {
                    this[e.type]();
                }
            }
        };

        function Button(data) {
            this.type = '';
            this.group = '';
            this.el = null;
            this.html = '';
            this.hidden = false;
            this.disabled = false;
            this.classes = [];
            this.name = '';
            this.value = '';
            this.label = '';
            this.toggleable = false;
            this.pressed = null;
            this.click = null;

            utils.setCfg(this, data);

            if (!data.type) {
                throw new Error('No button type specified');
            }

            if (Array.isArray(this.html)) {
                this.toggleable = true;
                this.pressed = false;
            }

            this.el = createSingle(data, true);
            this.el.addEventListener('click', this);
        }

        Button.prototype = {
            enable: function () {
                this.disabled = false;
                this.el.removeAttribute('disabled');
            },

            disable: function () {
                this.disabled = true;
                this.el.disabled = 'true';
            },

            toggle: function () {
                if (this.el.textContent == this.html[0]) {
                    this.pressed = true;
                    this.el.textContent = this.html[1];
                    this.el.setAttribute('aria-pressed', 'true');
                }
                else {
                    this.pressed = false;
                    this.el.textContent = this.html[0];
                    this.el.setAttribute('aria-pressed', 'false');
                }
            },

            handleEvent: function (e) {
                switch (e.type) {
                    case 'click':
                        if (this.toggleable) {
                            this.toggle();
                        }

                        if (typeof this.click == 'function') {
                            this.click();
                        }
                        else {
                            utils.triggerCustomEvent(e.target, this.type, {
                                group: this.group,
                                pressed: this.pressed
                            });
                        }
                }
            }
        };

        return {
            create: create,
            construct: construct
        };
    }
);
define(
    'feedback',['utils'],
    function (utils) {
        "use strict";

        /**
         *
         * @param cfg
         * @param container
         * @param buttons
         * @constructor
         */
        function Feedback(cfg, container, buttons) {
            this.container = container || document.getElementsByTagName('body')[0];
            this.el = utils.createElement('div', 'general-feedback');
            this.type = 'general';
            this.attempts = 0;
            this.allowedAttempts = 1000;
            this.correct = 0;
            this.total = 0;
            this.orderless = false; //forces an indexOf check rather than by index if true
            this.alternates = false; //counts arrays that don't have nested arrays in getTotal if true
            this.visible = false;
            this.marked = [];
            this.allCorrect = false;
            this.maxAttemptsReached = false;
            this.feedback = {
                general: [
                    'Your answer is incorrect.',
                    'Your answer is correct.',
                    'Your answer is partially correct.'
                ],
                score: [
                    'All of your answers are correct',
                    ' of your answers are correct',
                    ' of your answers is correct'
                ],
                specific: [],
                branch: [
                    'That is one possible answer.',
                    'That is the other possible answer.',
                    'That is another possible answer.'
                ],
                final: {
                    branch: {
                        general: [], //shown in order answered, e.g. first answered will show [0]
                        specific: [] //shown relative to specific branch
                    },
                    exercise: [
                        'Try another question.',
                        'That is the end of the exercise.'
                    ]
                },
                correct: '',
                incorrect: '',
                partial: '',
                always: ''
            };

            if (cfg) {
                utils.extendDeep(this, cfg);
            }

            //this.incrementTotal = (this.total === 0);
            //console.log(this.total);

            if (buttons) {
                this.buttons = buttons;
            }

            this.setControllers();
        }

        Feedback.prototype = {
            get: function() {
                var fb = this.feedback,
                    str;

                switch (this.type) {
                    case 'general':
                        switch (this.correct) {
                            case 0:
                                str = '<p>' + fb.general[0] + '</p>';
                                break;
                            case this.total:
                                str = '<p>' + fb.general[1] + '</p>';
                                break;
                            default:
                                str = '<p>' + fb.general[2] + '</p>';
                        }
                        break;
                    case 'score':
                        switch (this.correct) {
                            case 0:
                                str = '<p>None ' + fb.score[1] + '.</p>';
                                break;
                            case 1:
                                str = '<p>' + utils.capFirstLetter(utils.inWords(this.correct)) + fb.score[2] + '.</p>';
                                break;
                            case this.total:
                                str = '<p>' + fb.score[0] + '.</p>';
                                break;
                            default:
                                str = '<p>' + utils.capFirstLetter(utils.inWords(this.correct)) + fb.score[1] + '.</p>';
                        }
                }

                //response specific
                /*if (this.specific.length > 0 && index >= 0) {
                    str += ' ' + this.specific[index];
                 }*/

                //correct/incorrect
                switch (this.correct) {
                    case 0:
                        str += (fb.incorrect.length ? ' <p>' + fb.incorrect + '</p>' : '');
                        break;
                    case this.total:
                        str += (fb.correct.length ? ' <p>' + fb.correct + '</p>' : '');
                        break;
                    default:
                        str += (fb.partial.length ? ' <p>' + fb.partial + '</p>' : (fb.incorrect.length ? ' <p>' + fb.incorrect + '</p>' : ''));
                }

                str += fb.always.length ? ' <p>' + fb.always + '</p>' : '';

                if (this.attempts == this.allowedAttempts) {
                    this.maxAttemptsReached = true;
                    str += '<p>The correct answer is now shown.</p>';
                    utils.triggerCustomEvent(this.el, 'max-attempts-reached');

                    if (this.buttons.active('check')) {
                        this.buttons.get('check').disable();
                    }
                    if (this.buttons.active('reveal')) {
                        this.buttons.get('reveal').disable();
                    }
                }

                return str;
            },

            show: function(attempt, answer) {
                var bb;

                if (this.visible) {
                    this.hide();
                }

                this.attempts++;
                this.marked.splice(0);
                this.correct = 0;

                if (this.total === 0) {
                    this.getTotal(answer);
                }

                this.check(attempt, answer);
                this.calculate();
                this.trigger();

                console.log(attempt, answer);
                console.log(this.correct, this.total);

                this.el.textContent = '';
                this.el.insertAdjacentHTML('beforeend', '<p>' + this.get() + '</p>');
                this.el.style.display = 'block';
                this.visible = true;

                if (!this.maxAttemptsReached && !this.allCorrect) {
                    if (this.buttons.active('retry')) {
                        this.el.appendChild(this.buttons.get('retry').el);
                    }
                    else {
                        this.el.insertAdjacentHTML('beforeend', '<p>Try again.</p>');
                        utils.triggerCustomEvent(this.el, 'retry', { auto: true });
                    }
                }

                if (VLE && VLE.serverversion) {
                    VLE.resize_iframe();
                }

                ///TODO check behaviour!
                if (window.frameElement) {
                    bb = window.frameElement.getBoundingClientRect();

                    if (bb.bottom > window.top.innerHeight) {
                        //window.top.scrollTo(0, window.top.pageYOffset + bb.bottom - window.top.innerHeight);
                    }
                }
                else {
                    //window.scrollTo(0, document.body.scrollHeight);
                }
            },

            /**
             * Compares attempt to answer and stores result in this.marked
             * @param {object[]} attempt - an array/array of nested arrays etc.
             * @param {object[]} answer - an array/array of nested arrays etc.
             * @param {object[]} [marked] - the result of the comparison, same structure as input arrays with boolean/null values
             */
            check: function(attempt, answer, marked) {
                marked = marked || this.marked;
                attempt = Array.isArray(attempt) ? attempt : [attempt];
                answer = Array.isArray(answer) ? answer : [answer];

                attempt.forEach(function(attempt, i) {
                    if (Array.isArray(attempt)) {
                        marked[i] = marked[i] || [];
                        this.check(attempt, answer[i], marked[i]);
                    }
                    else {
                        if (attempt === null) {
                            marked[i] = null;
                            return;
                        }

                        if (Array.isArray(answer[i])) {
                            console.log(attempt, answer[i]);
                            marked[i] = (answer[i].indexOf(attempt) > -1) ? 1 : 0;
                        }
                        else {
                            if (this.orderless) {
                                marked[i] = (answer.indexOf(attempt) > -1) ? 1 : 0;
                            }
                            else {
                                marked[i] = (attempt === answer[i]) ? 1 : 0;
                                //console.log(marked[i], attempt, answer[i]);
                            }
                        }
                    }
                }.bind(this));
            },

            getTotal: function(answer) {
                answer.forEach(function(answer) {
                    if (Array.isArray(answer)) {
                        if (this.alternates && !Array.isArray(answer[0])) {
                            this.total++;
                        }
                        else {
                            this.getTotal(answer);
                        }
                    }
                    else {
                        if (answer !== null) {
                            this.total++;
                        }
                    }
                }.bind(this));
            },

            calculate: function(marked) {
                marked = marked || this.marked;

                marked.forEach(function(marked) {
                    if (Array.isArray(marked)) {
                        this.calculate(marked);
                    }
                    else {
                        if (marked !== null) {
                            this.correct = marked ? ++this.correct : this.correct;
                        }
                    }
                }.bind(this));
            },

            trigger: function() {
                var event;

                switch (true) {
                    case this.correct === 0:
                        event = 'incorrect';
                        break;
                    case this.correct > 0 && this.correct < this.total:
                        event =  'partially-correct';
                        break;
                    default:
                        event = 'correct';
                        this.allCorrect = true;
                }

                utils.triggerCustomEvent(this.el, event);
            },

            hide: function() {
                this.el.textContent = '';
                this.el.style.display = 'none';
                this.visible = false;

                if (!this.allCorrect && !this.maxAttemptsReached) {
                    this.buttons.enable('check');
                }

                if (VLE && VLE.serverversion) {
                    VLE.resize_iframe();
                }
            },

            reset: function() {
                this.attempts = 0;
                this.allCorrect = false;
                this.maxAttemptsReached = false;
                this.marked.splice(0);
                this.hide();
            },

            setControllers: function() {
                this.container.addEventListener('feedback', this);
                this.container.addEventListener('reveal', this);
                this.container.addEventListener('retry', this);
                this.container.addEventListener('reset', this);
            },

            handleEvent: function(e) {
                switch (e.type) {
                    case 'feedback':
                        this.show(e.detail.attempt, e.detail.answer);
                        break;
                    case 'reveal':
                    case 'retry':
                        if (e.detail && !e.detail.auto) {
                            this.hide();
                        }
                        break;
                    case 'reset':
                        this.reset();
                }
            }
        };

        return {
            construct: function(cfg, container, buttons) {
                return new Feedback(cfg, container, buttons);
            }
        };
    }
);
define(
	'input',['utils'],
	function (utils) {
		"use strict";

		var groups = [],
			types = {},
			aliases = {
				checkbox: 'cb',
				textarea: 'ta',
				text: 'tx',
				radio: 'ra',
				select: 'st'
			};

		/**
		 * @param {object||object[]} data
		 * @param {string} data.type
		 * @param {object[]} data.labels [optional]
		 */
		var create = function(data) {
			createNamespace(data);

			if (Array.isArray(data) && data.length > 1) {
				return createGroup(data);
			}
			else {
				data = Array.isArray(data) ? data[0] : data;

				if (data.type == 'radio' && !data.name) {
					throw new Error('Name required with type radio.');
				}

				return createSingle.call(types[data.type], data);
			}
		};

		var createNamespace = function(data) {
			data = Array.isArray(data) ? data : [data];

			data.forEach(function(data) {
				//check alias exists and throw error if not
				if (typeof aliases[data.type] == 'undefined') {
					throw new Error('Unsupported input type: ' + data.type);
				}

				//create namespace for type if undefined
				if (typeof types[data.type] == 'undefined') {
					types[data.type] = {
						index: 0
					};
				}
			});
		};

        var createGroup = function(data) {
            var group = utils.createElement('div', 'input-group'),
                num = Object.keys(groups).length,
                type = false;

            groups.push(0);

            data.forEach(function(data, i) {
                if (i === 0) {
                    type = data.type;
                }
                else {
                    type = (data.type == type) ? type : false;
                }

                data.group = groups.length;
                data.index = i;
                //data.name = data.name || 'input-group' + num;

                group.appendChild(createSingle.call(types[data.type], data, true));
            });

            data.group = group;
            data.type = type;

            return group;
        };

        /**
		 * @param {object} data
		 * @param {string} data.type
		 * @param {string|object|Element} data.label
		 * @param {string} [data.name]
		 * @param {string} [data.value]
		 * @param {boolean} [data.disabled]
		 * @param {boolean} [data.hidden]
		 * @param {function} [data.change]
		 * @param {object[]} [data.classes]
		 * @param {Element} data.el
		 * @param {boolean} group
		 */
		var createSingle = function(data, group) {
			var tag = getTagName(data.type),
				input = utils.createElement(tag, 'ou-input'),
				label = createLabel.call(this, input, data);

			this.index++;

			if (group) {
			    //console.log(groups);
				//groups[data.group] = ++groups[data.group] || 0;
				input.setAttribute('data-input-group', data.group);
				input.setAttribute('data-input-group-index', data.index);
				input.setAttribute('data-input-type', data.type);
			}

			if (tag == 'input') {
				input.type = data.type;
			}

			if (data.value) {
				input.value = data.value;
            }

			if (data.name) {
				input.name = data.name;
			}

			if (data.disabled) {
				input.disabled = true;
			}

			if (data.hidden) {
				(data.label instanceof Element ? input : label).style.display = 'none';
			}

			if (Array.isArray(data.classes)) {
				(data.label instanceof Element ? input : label).className = data.classes.join(' ');
			}

			if (initSpecific[data.type]) {
				initSpecific[data.type](data, input);
			}

			if (data.change) {
				input.addEventListener('change', data.change);
			}

			data.el = input;

            //return label if newly created, otherwise the input
            return Array.isArray(data.label) ? input : label;
		};

		var initSpecific = {
			/**
			 *
			 * @param data
			 * @param input
			 * @param {boolean} [data.checked]
			 */
			checkbox: function(data, input) {
				if (data.checked) {
					input.checked = data.checked;
				}
			},

			radio: function(data, input) {
				if (data.checked) {
					input.checked = data.checked;
				}
			},

			/**
			 *
			 * @param data
			 * @param input
			 * @param {object[]} [data.options]
			 * @param {object[]} [data.value]
			 * @param {boolean} [data.shuffle]
			 * @param {boolean} [data.placeholder]
			 */
			select: function(data, input) {
				var tmp = [],
					option;

				data.placeholder = (data.placeholder !== false);

				data.options.forEach(function(opt, i, arr) {
					if (i === 0 && data.placeholder) {
						option = document.createElement('option');
						option.textContent = 'Select...';
						option.selected = true;
						option.disabled = true;
						option.hidden = true;
						input.appendChild(option);
					}

					option = document.createElement('option');
					tmp.push(option);

					if (typeof opt == 'string') {
						opt = arr[i] = {
							label: opt
						};
					}

					if (opt.label) {
						option.textContent = opt.label;
					}

					option.value = opt.value || i;

					if (opt.disabled) {
						option.disabled = true;
					}

					if (opt.selected) {
						option.selected = true;
					}
				});

				if (data.shuffle) {
					utils.shuffle(tmp);
				}

				tmp.forEach(function(option) {
					input.appendChild(option);
				});
			},

			text: function(data, input) {
				var min = (typeof data.min == 'number') ? data.min : null,
					max = (typeof data.max == 'number') ? data.max : null;

				if (data.valid == 'number') {
					input.addEventListener('input', function() {
						this.value = this.value.replace(/[^\d]+/g, '');
					});
				}

				if (min || max) {
					data.change = function(change, e) {
						var val = parseInt(e.target.value, 10);

						switch (true) {
							case isNaN(val):
								e.target.value = min || 0;
								break;
							case min && val < min:
								e.target.value = min;
								break;
							case max && val > max:
								e.target.value = max;
						}

						if (typeof change == 'function') {
							change.call(this, e);
						}
					}.bind(input, data.change);
				}
			}
		};

		/**
		 * @type {HTMLElement}
		 * @param {HTMLElement} input
		 * @param {object} data
		 * @param {string|object|Element} data.label
		 * @param {string} [data.label.prefix]
		 * @param {string} [data.label.suffix]
		 */
		var createLabel = function(input, data) {
			if (!data.label) {
				throw new Error('Label data missing!');
			}

			var id = aliases[data.type] + this.index,
                label, span, val;

			input.id = id;

			if (data.label instanceof Element) {
				data.label = [data.label];
			}

            if (Array.isArray(data.label)) {
                label = data.label;
            }
            else {
				label = document.createElement('label');
				label.appendChild(input);

				if (typeof data.label == 'string') {
					val = data.label;

					data.label = {
						suffix: val
					};
				}

				//stick spans around each part to help with styling later
				span = label.appendChild(document.createElement('span'));
				span.appendChild(input);

				if (data.label.prefix) {
					label.insertAdjacentHTML('afterbegin', '<span class="label-prefix">' + data.label.prefix + '</span>');
				}
				if (data.label.suffix) {
					label.insertAdjacentHTML('beforeend', '<span class="label-suffix">' + data.label.suffix + '</span>');
				}
			}

			//wrap label in array (if not already) to work with it
			label = Array.isArray(label) ? label : [label];

			label.forEach(function(label) {
				val = label.getAttribute('for') || '';
				label.setAttribute('for', (val ? val + ' ' : '') + id);
			});

			return (label.length == 1) ? label[0] : label;
        };

		var isInput = function(type) {
			return !!aliases[type];
		};

		var getTagName = function(type) {
			return (type == 'radio' || type == 'checkbox' || type == 'text' || type == 'range') ? 'input' : type;
		};

		return {
			create: create,
			isInput: isInput
		};
	}
);

define(
    'saver',[],function () {
        "use strict";
        var online = (VLE && VLE.serverversion) || false,
            local = (typeof Storage !== 'undefined'),
            activityId = '',
            itemId = '',
            courseId = '',
            saveMsg = document.getElementById('save-msg') || null;

        var retrieve = function (cfg) {
            cfg.user = (cfg.user !== false); //true unless == false
            if (typeof cfg.callback != 'function') {
                cfg.callback = function () {
                };
            }
            if (online) {
                retrieveDataFromVLE(cfg);
            }
            else {
                if (local) {
                    retrieveDataFromLocalStorage(cfg);
                }
            }
        };

        var retrieveDataFromVLE = function (cfg) {
            var ok = function (data) {
                var outObj = {};
                for (var k in data) {
                    if (data.hasOwnProperty(k)) {
                        outObj[k] = decodeURIComponent(data[k]);
                    }
                }
                cfg.callback(outObj);
                console.log('Retrieved ' + ((cfg.user) ? 'user' : 'group') + ' data: ', outObj);
            };
            var error = function (msg) {
                if (msg !== null) {
                    console.log(msg);
                }
                cfg.callback({});
                console.log('get_server_data has failed.');
            };
            var names = [];
            var p;
            for (p in cfg.names) {
                if (cfg.names.hasOwnProperty(p)) {
                    names.push(p);
                }
            }
            console.log('Data to be retrieved: ' + names);
            VLE.get_server_data(cfg.user, names, ok, error, activityId, itemId, courseId);
        };

        var retrieveDataFromLocalStorage = function (cfg) {
            var outObj = {},
                name, item, p;

            for (p in cfg.names) {
                if (cfg.names.hasOwnProperty(p)) {
                    name = p;
                    item = localStorage.getItem(activityId + name);
                    if (item === null) {
                        outObj[name] = '';
                    } else {
                        outObj[name] = decodeURIComponent(item);
                    }
                }
            }

            cfg.callback(outObj);
            console.log('Retrieved ' + (cfg.user ? 'user' : 'group') + ' data: ', outObj);
        };

        /*
         * @param {object} cfg
         * @param {boolean} cfg.user User, global, or group identifier
         * @param {object} cfg.values Data to be saved
         * @param {function} cfg.callback Callback function called on success/fail (returns true/false)
         * @param {object} cfg.previous Previously values
         * @param {function} cfg.retry Function that is called if previous values changed
         * @param {string} cfg.id
         */
        var save = function (cfg) {
            var saveObj = {};

            cfg.user = (cfg.user !== false); //true unless == false

            if (typeof cfg.callback != 'function') {
                cfg.callback = function () {
                };
            }

            if (saveMsg !== null) {
                fadeSaveMsg();
            }

            console.log(cfg.values);

            for (var p in cfg.values) {
                if (cfg.values.hasOwnProperty(p)) {
                    saveObj[p] = encodeURIComponent(cfg.values[p]);
                }
            }

            console.log((cfg.user ? 'User' : 'Group') + ' data to be saved: ', cfg.values);

            if (online) {
                saveDataToVLE(saveObj, cfg);
            }
            else { /* don't use local storage unless offline */
                if (local) {
                    saveToLocalStorage(saveObj, cfg);
                }
                else {
                    cfg.callback(false);
                    saveMsg.textContent = 'cannot save offline';
                }
            }
        };

        var fadeSaveMsg = function () {
            var opacity = 0;
            var fadeIn, fadeOut;
            fadeIn = (function fadeIn() {
                saveMsg.style.opacity = (opacity += (1 / 30));
                if (opacity < 1) {
                    requestAnimationFrame(fadeIn.bind(this));
                }
                else {
                    setInterval(fadeOut, 500);
                }
            }.bind(this))();
            fadeOut = function fadeOut() {
                saveMsg.style.opacity = (opacity -= (1 / 30));
                if (opacity > 0) {
                    requestAnimationFrame(fadeOut.bind(this));
                }
            }.bind(this);
        };

        var saveDataToVLE = function (saveObj, cfg) {
            var user = (cfg.user !== false);
            var ok = function () {
                cfg.callback(true);
                console.log('set_server_data (' + (user ? 'user' : 'group') + ') was successful');
            };
            var error = function (msg) {
                if (msg !== null) {
                    console.log(msg);
                }
                cfg.callback(false);
                console.log('set_server_data has failed');
            };
            var previousValues = cfg.previous;
            var retry = cfg.retry || null;
            console.log(saveObj);
            VLE.set_server_data(user, saveObj, ok, error, previousValues, retry, activityId, itemId, courseId);
        };

        var saveToLocalStorage = function (saveObj, cfg) {
            //TODO alert message on first save - Any data saved in this context will only be available later on this device. Continue?
            for (var p in saveObj) {
                if (saveObj.hasOwnProperty(p)) {
                    localStorage.setItem(activityId + p, saveObj[p]);
                }
            }
            cfg.callback(true);
        };

        var makeIdFromOrigin = function () {
            var theId = '';
            var theOrigin, i;
            /*
             takes the alpha-numeric characters from the origin and pathname and uses
             them to make an 'activity id' which should be safe for use with the VLE
             */
            if (!window.location.origin) { /* i.e. IE */
                window.location.origin = window.location.protocol + "//" + window.location.hostname + (window.location.port ? ':' + window.location.port : '');
            }
            theOrigin = window.location.origin + window.location.pathname;
            for (i = 0; i < theOrigin.length; i++) {
                if (theOrigin.charCodeAt(i) > 96 && theOrigin.charCodeAt(i) < 123) {
                    theId += theOrigin.charAt(i);
                }
            }
            for (i = 0; i < theOrigin.length; i++) {
                if (theOrigin.charCodeAt(i) > 64 && theOrigin.charCodeAt(i) < 91) {
                    theId += theOrigin.charAt(i);
                }
            }
            for (i = 0; i < theOrigin.length; i++) {
                if (theOrigin.charCodeAt(i) > 47 && theOrigin.charCodeAt(i) < 58) {
                    theId += theOrigin.charAt(i);
                }
            }
            // since this id is only to be used for local storage it can be more than 20 characters
            return theId;
        };

        if (online) {
            activityId = VLE.get_param('activityId') || VLE.get_param('_a');
            itemId = VLE.get_param('documentId') || VLE.get_param('_i');
            courseId = VLE.get_param('courseId') || VLE.get_param('_c');
        }
        else {
            activityId = makeIdFromOrigin();
        }

        return {
            retrieve: retrieve,
            save: save
        };
    });
define('spinner',[
		'utils'
	],
	function(utils) {
        "use strict";

        function Spinner(parent) {
            this.container = utils.createElement('div', 'loader-container');
            this.parent = parent || document.body;
            this.el = utils.createElement('div', 'loader');
            this.spinner = utils.createElement('div', 'loader-spinner');
            this.text = utils.createElement('div', 'loader-text');
            this.diameter = 40;
            this.animation = null;
        }

        Spinner.prototype = {
            init: function() {
                this.spinner.style.display = 'inline-block';
                this.spinner.style.border = Math.round(this.diameter / 7) + 'px solid #f3f3f3';
                this.spinner.style.borderTopColor = '#3498db';
                this.spinner.style.borderRadius = '50%';
                this.spinner.style.width = this.diameter + 'px';
                this.spinner.style.height = this.diameter + 'px';

                this.text.innerHTML = 'Loading...';
                this.text.style.marginTop = '8px';

                this.el.style.textAlign = 'center';
                this.el.style.position = 'absolute';
                this.el.style.top = '50%';
                this.el.style.left = '50%';
                this.el.style.transform = 'translate(-50%, -50%)';
                this.el.appendChild(this.spinner);
                this.el.appendChild(this.text);

                this.container.style.height = window.innerHeight + 'px';
                this.container.style.position = 'relative';
                this.container.style.visibility = 'visible';
                this.container.appendChild(this.el);

                this.parent.appendChild(this.container);
                this.parent.addEventListener('activity-loaded', this);

                this.start();
            },

            start: function() {
                var deg = 0,
                    unit = 8;

                (function spin() {
                    this.spinner.style.transform = 'rotate(' + (deg += unit) + 'deg)';
                    this.animation = requestAnimationFrame(spin.bind(this));
                }.bind(this))();
            },

            stop: function() {
                cancelAnimationFrame(this.animation);
                this.container.style.display = 'none';
            },

            handleEvent: function(e) {
                switch (e.type) {
                    case 'activity-loaded':
                        this.stop();
                }
            }
        };

        return {
            construct: function(parent) {
                return new Spinner(parent);
            }
        };
    }
);
define(
    'loader',['utils', 'spinner'],
    function(utils, spinner) {
        "use strict";
        /**
         * a general purpose loader for various file types/VLE calls
         * @constructor
         */
        function Loader() {
            this.el = null;
            this.spinner = null;
            this.loading = [];
            this.dir = '';
            this.data = null;
            this.types = {};
            this.callback = null;
        }

        Loader.prototype = {
            /**
             * common load function
             * @param {object} data - cfg object
             * @param {string} data.dir - directory to resolve all paths by
             * @param {object} data.load - items to be loaded broken down by type
             * @param {function} data.callback - callback function
             * @param {boolean} data.spinner - show spinner (default true)
             * @param {Element} data.container - containing element for spinner
             */
            load: function(data) {
                if (!data) {
                    console.log('No data passed to loader');
                    return;
                }

                this.data = {};
                this.dir = data.dir || '';
                this.loading.splice(0); //clear array
                this.callback = data.callback || console.log.bind(null, 'No callback provided!');

                if (data.spinner !== false) {
                    this.spinner = spinner.construct(data.container);
                    this.spinner.init();
                }

                this.queue(data.load);
            },

            /**
             * loops data passed for each type and calls relevant get
             * @param {object} data - items to be loaded broken down by type
             */
            queue: function(data) {
                var type;

                utils.forIn(data, function(list, key) {
                    if (typeof list == 'string') {
                        //list = [list];
                    }

                    if (!Array.isArray(list) || !list.length || !this.get[key]) {
                        return;
                    }

                    type = this.getType(key);

                    if (this.loading.indexOf(type.name) < 0) {
                        this.loading.push(type.name);
                    }

                    list.forEach(function(data) {
                        type.loaded.push(null);
                        type.data.push(data);

                        this.get[type.name](this, type, data, (type.data.length - 1));
                    }.bind(this));

                    console.log(type.name + ' queued');
                }.bind(this));

                //if there's nothing to load...
                if (!this.loading.length) {
                    this.complete();
                }
            },

            /**
             * returns type object (sets up first if undefined)
             * @param type
             * @returns {object}
             */
            getType: function(type) {
                if (!this.types[type]) {
                    this.types[type] = {
                        name: type,
                        data: [],
                        loaded: []
                    };
                }

                return this.types[type];
            },

            /**
             * holds get functions for various types
             */
            get: {
                /**
                 * loads an image
                 * @param {Loader} loader - reference to this
                 * @param {object} type - type data object
                 * @param {string} src - url (relative or absolute)
                 * @param {number} i - item type index
                 */
                image: function(loader, type, src, i) {
                    var img = new Image();

                    img.src = loader.dir + src;
                    img.onload = function() {
                        console.log(type.name + '[' + i + ']' + ' loaded');

                        type.loaded[i] = {
                            src: src,
                            width: this.naturalWidth,
                            height: this.naturalHeight
                        };

                        loader.itemLoaded(type.name);
                    };
                },

                /**
                 * gets a files contents (what exactly is determined by format)
                 * @param {Loader} loader - reference to this
                 * @param {object} type - type data object
                 * @param {string} src - url (relative or absolute)
                 * @param {number} i - item type index
                 */
                file: function(loader, type, src, i) {
                    var xhr = new XMLHttpRequest(),
                        format = src.split('.').pop(),
                        doc;

                    src = loader.dir + src;

                    xhr.onreadystatechange = function() {
                        if (xhr.readyState == 4) {
                            if (xhr.status == 200 || xhr.status === 0) {
                                console.log(type.name + '[' + i + ']' + ' loaded');

                                switch (format) {
                                    case 'svg':
                                    case 'xml':
                                        if (window.DOMParser) {
                                            doc = new DOMParser().parseFromString(xhr.responseText, 'text/xml');
                                        }
                                        else {
                                            //IE9 support...
                                            doc = new ActiveXObject('Microsoft.XMLDOM');
                                            doc.async = false;
                                            doc.loadXML(xhr.responseText);
                                        }
                                        doc = doc.documentElement;
                                        break;
                                    case 'json':
                                        doc = JSON.parse(xhr.responseText);
                                        break;
                                    default:
                                        doc = xhr.responseText;
                                }

                                type.loaded[i] = doc;
                                loader.itemLoaded(type.name);
                            }
                        }
                    };

                    xhr.open('GET', src);
                    xhr.send();
                },

                /**
                 * gets
                 * @param {Loader} loader - reference to this
                 * @param {object} type - type data object
                 * @param {object[]} data - doc name and part ([name, part])
                 * @param {number} i - item type index
                 */
                olink: function(loader, type, data, i) {
                    VLE.get_olink_url(
                        data[0],
                        data[1] || '',
                        function(url) {
                            console.log(type.name + '[' + i + ']' + ' loaded');

                            type.loaded[i] = url.replace(/&/g, '&amp;');
                            loader.itemLoaded(type.name);
                        },
                        function(error) {
                            console.log(type.name + '[' + i + ']' + ' failed to load', error);

                            type.loaded[i] = 'javascript:void(0)';
                            loader.itemLoaded(type.name);
                        },
                        VLE.get_param('_c') //courseid
                    );
                },

                /**
                 * loads an image
                 * @param {Loader} loader - reference to this
                 * @param {object} type - type data object
                 * @param {string} name - name attribute of Folder tag (SC)
                 * @param {number} i - item type index
                 */
                folder: function(loader, type, name, i) {
                    var formats = {
                        file: ['css', 'json', 'js'],
                        image: ['jpg', 'png', 'svg']
                    };
                    var load = {}, format;

                    VLE.get_folder(name,
                        function(contents) {
                            console.log(type.name + '[' + i + ']' + ' loaded');

                            //load contents by type relative to format
                            contents.some(function(data) {
                                format = data.path.split('.').pop();

                                utils.forIn(formats, function(list, key) {
                                    if (list.indexOf(format) > -1) {
                                        load[key] = load[key] || [];
                                        load[key].push(data.path);
                                        console.log(data);
                                    }
                                });

                                switch (format) {
                                    case 'css':
                                        contents.css = data.url;
                                        break;
                                    case 'json':
                                        contents.json = load.file.length - 1;
                                }

                            });

                            loader.dir = contents[0].url.match(/.+?(?=zip)/) + 'zip//';
                            loader.queue(load);

                            type.loaded[i] = contents;
                            loader.itemLoaded(type.name);
                        },
                        function(error) {
                            console.log(type.name + '[' + i + ']' + ' failed to load', error);

                            type.loaded[i] = [];
                            loader.itemLoaded(type.name);
                        });
                }
            },

            /**
             * checks whether items for given type are all loaded
             * @param {string} type - type name
             * @returns {boolean}
             */
            isLoaded: function(type) {
                //compare data and loaded lengths and, if true, check all values are truthy
                return this.types[type].loaded.length == this.types[type].data.length && this.types[type].loaded.every(function(data) { return !!data; });
            },

            /**
             * resets type data if all items for a given type are loaded and calls this.complete
             * if all items for all types are loaded.
             * @param {string} type - type name
             */
            itemLoaded: function(type) {
                var index;

                if (this.isLoaded(type)) {
                    index = this.loading.indexOf(type);

                    if (index > -1) {
                        //remove type from loading array
                        this.loading.splice(index, 1);

                        //set type data on return object
                        this.data[type] = this.types[type].loaded.splice(0);
                        this.types[type].data = [];

                        console.log(type + ' loaded');
                    }
                }

                if (this.loading.length === 0) {
                    this.complete();
                }
            },

            /**
             * wraps everything up and calls this.callback
             */
            complete: function() {
                var json, path;

                console.log('everything loaded');

                if (this.data.folder) {
                    this.data.folder.forEach(function(contents) {
                        if (typeof contents.json == 'number') {
                            json = this.data.file[contents.json];

                            //ensure JSON is at index 0
                            this.data.file.unshift(this.data.file.splice(contents.json, 1)[0]);

                            if (this.data.image.length) {
                                path = this.data.image[0].src.replace(/([^/]+)$/, '');
                                json.images = json.images || {};
                                json.images.dir = this.dir;
                                json.images.files = json.images.files || [];
                                json.images.files.forEach(function(str, i) {
                                    json.images.files[i] = path + str;
                                });
                            }

                            //if css, set url on JSON object
                            if (contents.css) {
                                json.css = contents.css;
                            }
                        }
                    }.bind(this));
                }

                this.callback(this.data);
            }
        };

        return {
            /**
             *
             * @returns {Loader}
             */
            construct: function() {
                return new Loader();
            }
        };
    }
);

