/**
 * IAT - jQuery plugin for creating an Implicit Association Test (IAT)
 *
 * @author    Timo Gnambs <timo@gnambs.at>
 * @copyright (c) 2008 Timo Gnambs (http://timo.gnambs.at)
 * @license   Dual licensed under the MIT and GPL licenses:
 *              http://www.opensource.org/licenses/mit-license.php
 *              http://www.gnu.org/licenses/gpl.html
 * @requires  jQuery v1.2.1
 * @version   2008-04-10
 */


(function($) {
    
    /**
     * Creates an online Implicit Association Test (IAT), a psychological test aimed to measure implicit attitudes.
     * For more information on these kind of tests see http://projectimplicit.net 
     *
     * NOTE: There is no warranty at all that this online test does indeed and can measure
     *       the same construct as the orignial test. If you use this script do it on your own risk!
     *
     * Instruction:
     * The script is invoked by including the following three script files and one style file in the head
     * section of your html file and initialising the script as shown in the examples:
     *   jquery.js          (the script library for DOM manipulation)
     *   jquery.hotkeys.js  (an additional jQuery plugin to log keyboard keys)
     *   jquery.iat.js      (the actual script to generate the IAT)
     *   jquery.iat.css     (required styles for the IAT to display correctly)
     *
     * @example  $('#container').iat();
     * @desc     Creates the IAT with the default settings
     * @example  $('#container').iat(mySettings);
     * @desc     Creates the IAT with your settings as defined in the variable mySettings
     * @example  $('#container').iat(mySettings, myMessages);
     * @desc     Creates the IAT with your settings as defind in the variable mySettings and
     *           and with your messages as defined in myMessages
     *
     * @param    Object          settings    An object defining the appearance of the IAT;
     *                                       NOTE: all settings are optional
     * @option   Array<String>   categories  An array containing the left and right category labels;
     *                                       The following array would create an IAT block with "me" in the left and 
     *                                       "others" in the right top corner: [ "me", "others" ]
     * @option   Array<String>   targets     A multidimensional array containing the targets for the categories;
     *                                       The following array would create an IAT block with "me" in the left and 
     *                                       "others" in the right top corner
     *                                       [ "me", "others" ]
     * @option   Array<String>   keys        Assigend keyboard keys; e.g. 'f', 'F1', 'Space'
     *                                       see jquery.hotkeys.js for details
     *                                       [0] ... key for start page
     *                                       [1] ... key for left category
     *                                       [2] ... key for right category
     * @option   String          sequence    Determines the sequence of targets; valid values are:
     *                                       "random": targets are presented in random order
     *                                       "fixed": targets are presented in the order as specified in targets
     * @option   Number          runs        Number of times to display each target
     * @option   String          frmid       The id of the dynamically created form field to save the results to
     * @option   Array<String>   separator   Separator for saves values: 
     *                                       [0] ... values of one target (category index : target index : key pressed : time)
     *                                       [1] ... between targets
     * @option   Number          dispcats    When to display categories:
     *                                       0 ... always
     *                                       1 ... donīt display during instruction
     *                                       2 ... never
     * @option   Boolean         dispfalse   Display feedback for incorrect targets
     * @option   Boolean         disptrue    Display feedback for correct targets
     * @option   Boolean         report      Report IAT results on last page
     * @option   Function        onload      A hook to run a specific function before loading the IAT
     * @option   Function        onstart     A hook to run a specific function when the user has started the IAT, right before displaying 
     *                                       the first target word
     * @option   Function        onend       A hook to run a specific function when the user has reacted to the last target word
     * @option   Boolean         debug       Turn debugging on, which displays alert messages when encountering erros and displays 
     *                                       the total time to create the IAT right under the test
     *
     * @param    Object          messages    An object defining different texts used in creating the IAT;
     *                                       NOTE: all messages are optional
     * @option   String          start       Text to display before the IAT starts
     * @option   String          end         Text to display after the IAT has finished; you may use the following place holders:
     *                                         %true%:  number of correct classified targets
     *                                         %false%:  number of incorrect classified targets
     *                                         %mean%:  mean reaction time
     *                                         %long%:  longest reaction time
     *                                         %short%: shortest reaction time
     * @option   String          instruction Instruction on first page
     * @option   String          wrong       Feedback for incorrect targets
     * @option   String          correct     Feedback for correct targets
     *
     * @type     jQuery
     * @name     iat
     * @author   Timo Gnambs <timo@gnambs.at>
     */
    $.fn.iat = function(settings, messages){
        
        /** settings */
        settings = $.extend({
            categories : ["ich<br/>Esel", "nicht ich"],             /** categories to sort target words to */
            targets: [ ["mein", 0],                                 /** target words to categorize */
                       ["selbst", 0],
                       ["ich", 0],
                       ["mich", 0],
                       ["euer", 1],
                       ["andere", 1],
                       ["es", 1],
                       ["sie", 1] ],
            keys: ["space", "a", "l"],                              /** keybord keys */
            sequence: 'random',                                     /** display targets in 'fixed' or 'random' sequence */  
            runs: 2,                                                /** number of times to display targets */
            frmid: "iatdata",                                       /** id of hidden form element to save data to */
            separator: [":", ";"],                                  /** value separators in hidden form field (0: in target, 1: between targets) */
            delay: 300,                                             /** time delay between two target words (in ms) */
            dispcats: 0,                                            /** when to display categories (0: always, 1: not on first page, 2: never */
            dispfalse: 1,                                           /** display feedback for incorrect categorizations */
            disptrue: 0,                                            /** display feedback for correct categorizations */
            report: 1,                                              /** report iat results on last page */
            onload: function()  {},                                 /** function to run before loading the script */
            onstart: function() {},                                 /** function to run on test start */
            onend: function() { },                                  /** function to run on test end */
            debug: false                                            /** debug mode */
        },  settings || {});
        
                
        /** messages */
        messages = $.extend({
            instruction: "<p>Put your middle index fingers on the <b>E</b> and <b>I</b> keys of your keyboard. Try categorizing the words, which will be appearing in the middle of the screen, as fast as possible into one of the categories at the top by pressing the letters <b>E</b>, if the item belongs to one of the left categories, or <b>I</b>, if the item belongs to one of the right categories, on your keyboard.</p><p style=\"text-align:center;color:red;font-weight: 600;\">Press SPACE to begin.</p>",
            wrong: "Wrong",
            correct: "Correct",
            report: "<p>Your results:</p><ul><li class=\".iatTrue\">Correct: %true%</li><li class=\".iatFalse\">Errors: %false%</li><li>Mean Reaction Time: %mean% sec</li><li>Longest Reaction Time: %long% sec</li><li>Fastest reaction time: %short% sec</li></ul>",
            end: "Thank your for your participation."
        },  messages || {});
        
        var me = $(this), arr = new Array(), c = -1, t = 0, active = false;
        
        /**
         * Set target word and save response
         *    NOTE: keep as simple as possible, to minimize CPU usage and possible contamination of reaction time measurement
         *
         * @param    int  
         * @return   void
         */
        function setTarget(key) {
        
            /** save response to array */
            if(key > 0) {
                if(!active) return;                         /** dont process key press when no target is displayed */
                arr[c][2] = key-1;                          /** key pressed */
                arr[c][3] = (new Date()).getTime() - t;     /** reaction time */
            }
            me.empty();
            active = false;
            
            if(key > 0 && settings.disptrue && arr[c][1] == arr[c][2]) me.html("<span class=\"iatTrue\">" + messages.correct + "</span>");
            else if(key > 0 && settings.dispfalse && arr[c][1] != arr[c][2]) me.html("<span class=\"iatFalse\">" + messages.wrong + "</span>");
            
            /** quit after last target*/
            if(c >= arr.length-1) {
                jQuery.hotkeys.remove(settings.keys[1]);
                jQuery.hotkeys.remove(settings.keys[2]);
                me.parent().find('.iatLeft').empty();
                me.parent().find('.iatRight').empty();
                me.empty();
                
                /** save data to hidden form field */
                var d = '';
                for(var i = 0; i < arr.length; i++) 
                    d += (i+1) + settings.separator[0] + arr[i].join(settings.separator[0]) + settings.separator[1];
                $("#" + settings.frmid).val(d);
                
                /** create report */
                if(settings.report) {
                    var correct = total = max = msg = 0, min = 99999;
                    for(var i = 0; i < arr.length; i++) {
                        if(arr[i][1] == arr[i][2]) correct++;
                        total += arr[i][3];
                        if(arr[i][3] > max) max = arr[i][3];
                        if(arr[i][3] < min) min = arr[i][3];
                    }
                    msg = (messages.report).replace(/%true%/, correct);
                    msg = msg.replace(/%false%/, arr.length - correct);
                    msg = msg.replace(/%long%/, Math.round(max/10)/100);
                    msg = msg.replace(/%short%/, Math.round(min/10)/100);
                    msg = msg.replace(/%mean%/, Math.round((total/arr.length)/10)/100);
                    me.parent().find('.iatInstruction').html(msg);
                } else {
                    me.parent().find('.iatInstruction').html(messages.end);
                }
                
                /** on end hook */
                settings.onend();
                
                return;
            }
            
            /** set next target word */
            c++;
            window.setTimeout( function() { me.html( settings.targets[ arr[c][0] ][0] ); active = true; t = (new Date()).getTime(); }, settings.delay);
        }
        
        
        /**
         * Return a random number to sort an array randomly
         *
         * @return int
         */
        function randOrder () {
            return (Math.round(Math.random())-0.5);
        }
        
        
        return $(this).each(function() {
            
            if(settings.debug) { var benchmark = (new Date()).getTime(); }
            
            /** on load hook */
            settings.onload();
          
            /** create html structure */
            $(this).empty().append('<div class="iatTop"><div class="iatLeft"></div><div class="iatRight"></div></div><div class="iatInstruction"></div><div class="iatTarget"></div>');
            
            /** create hidden form fields to save data to */
            $(this).after('<input type="hidden" value="" id="' + settings.frmid + '" name="' + settings.frmid + '" style="width:0;"/>');
            
            /** create temporary array holding the result set */
            for(var i = 0; i < settings.targets.length; i++) 
                arr[i] = new Array(i, settings.targets[i][1]);  //remove actual target content and keep id only
            if(settings.sequence == 'random') {                 //randomize order of temp array
                arr.sort( randOrder );  
                arr.sort( randOrder );
            }
            for(var i = 1; i < settings.runs; i++) arr = arr.concat(arr);  //duplicate temp array according to settings

            /** set start message */
            $(this).find('.iatInstruction').html(messages.instruction);
            
            /** set category labels */
            if(!settings.dispcats) {
                me.find('.iatLeft').html(settings.categories[0]);
                me.find('.iatRight').html(settings.categories[1]);
            }
                
            $.hotkeys.add(settings.keys[0], function() { 
            
                //remove key action for start key
                $.hotkeys.remove(settings.keys[0]);
                
                //set category labels
                if(settings.dispcats == 1) {
                    me.find('.iatLeft').html(settings.categories[0]);
                    me.find('.iatRight').html(settings.categories[1]);
                }
                me.find('.iatInstruction').empty();
                me = me.find('.iatTarget');
                
                //add key actions
                $.hotkeys.add(settings.keys[1], function() { setTarget(1); } );
                $.hotkeys.add(settings.keys[2], function() { setTarget(2); } );
                
                //on start hook
                settings.onstart();
                
                //display first target
                setTarget(0);
            } ); 
          
            /** add benchmark */
            if(settings.debug) {
                $(this).after('<div>Created in ' + (((new Date()).getTime()-benchmark)/1000) + ' sec.</div>');
            }
            
        });
        
    };
    
})(jQuery);


