/*
 * gameQuery rev. 0.7.1
 *
 * Copyright (c) 2012 Selim Arsever (http://gamequeryjs.com)
 * licensed under the MIT-License
 */

// This allows use of the convenient $ notation in a plugin
(function($) {
	
	// CSS Feature detection from: Craig Buckler (http://www.sitepoint.com/detect-css3-property-browser-support/)
	var cssTransform = false;
	
	var detectElement = document.createElement("detect"),  
    	CSSprefix = "Webkit,Moz,O,ms,Khtml".split(","),
    	All = ("transform," + CSSprefix.join("Transform,") + "Transform").split(",");  
	for (var i = 0, l = All.length; i < l; i++) {  
	    if (detectElement.style[All[i]] === "") {  
	          cssTransform = All[i];
	    }  
	}
	
    // This prefix can be use whenever needed to namespace CSS classes, .data() inputs aso.
    var gQprefix = "gQ_";
    
    // Those are the possible states of the engine
    var STATE_NEW     = 0; // Not yet started for the first time
    var STATE_RUNNING = 1; // Started and running 
    var STATE_PAUSED  = 2; // Paused
    
    /**
     * Utility function that returns the radius for a geometry.
     *
     * @param {object} elem DOM element
     * @param {float} angle the angle in degrees
     * @return {object} .x, .y radius of geometry
     */
    var proj = function (elem, angle) {
        switch (elem.geometry){
            case $.gameQuery.GEOMETRY_RECTANGLE :
                var b = angle*Math.PI*2/360;
                var Rx = Math.abs(Math.cos(b)*elem.width/2*elem.factor)+Math.abs(Math.sin(b)*elem.height/2*elem.factor);
                var Ry = Math.abs(Math.cos(b)*elem.height/2*elem.factor)+Math.abs(Math.sin(b)*elem.width/2*elem.factor);

                return {x: Rx, y: Ry};
        }
    };
    
    /**
     * Utility function that checks for collision between two elements.
     *
     * @param {object} elem1 DOM for the first element
     * @param {float} offset1 offset of the first element
     * @param {object} elem2 DOM for the second element
     * @param {float} offset2 offset of the second element
     * @return {boolean} if the two elements collide or not
     */
    var collide = function(elem1, offset1, elem2, offset2) {
        // test real collision (only for two rectangles...)
        if((elem1.geometry == $.gameQuery.GEOMETRY_RECTANGLE && elem2.geometry == $.gameQuery.GEOMETRY_RECTANGLE)){

            var dx = offset2.x + elem2.boundingCircle.x - elem1.boundingCircle.x - offset1.x;
            var dy = offset2.y + elem2.boundingCircle.y - elem1.boundingCircle.y - offset1.y;
            var a  = Math.atan(dy/dx);

            var Dx = Math.abs(Math.cos(a-elem1.angle*Math.PI*2/360)/Math.cos(a)*dx);
            var Dy = Math.abs(Math.sin(a-elem1.angle*Math.PI*2/360)/Math.sin(a)*dy);

            var R = proj(elem2, elem2.angle-elem1.angle);

            if((elem1.width/2*elem1.factor+R.x <= Dx) || (elem1.height/2*elem1.factor+R.y <= Dy)) {
                return false;
            } else {
                var Dx = Math.abs(Math.cos(a-elem2.angle*Math.PI*2/360)/Math.cos(a)*-dx);
                var Dy = Math.abs(Math.sin(a-elem2.angle*Math.PI*2/360)/Math.sin(a)*-dy);

                var R = proj(elem1, elem1.angle-elem2.angle);

                if((elem2.width/2*elem2.factor+R.x <= Dx) || (elem2.height/2*elem2.factor+R.y <= Dy)) {
                    return false;
                } else {
                    return true;
                }
            }
        } else {
            return false;
        }
    };
    
    /** 
     * Utility function computes the offset relative to the playground of a gameQuery element without using DOM's position.
     * This should be faster than the standand .offset() function.
     * 
     * Warning: No non-gameQuery elements should be present between this element and the playground div!
     * 
     * @param {jQuery} element the jQuery wrapped DOM element representing the gameQuery object.
     * @return {object} an object {x:, y: } containing the x and y offset. (Not top and left like jQuery's .offset())  
     */
    var offset = function(element) {
        // Get the tileSet offset (relative to the playground)
        var offset = {x: 0, y: 0};
        var parent = element[0];
        
        while(parent !== $.gameQuery.playground[0] && parent.gameQuery !== undefined) {
            offset.x += parent.gameQuery.posx;
            offset.y += parent.gameQuery.posy;
            parent = parent.parentNode;
        }
        
        return offset
    }
    
    /**
     * Utility function computes the index range of the tiles for a tilemap.
     * 
     * @param {jQuery} element the jQuery wrapped DOM element representing the tilemap.
     * @param {object} offset an object holding the x and y offset of the tilemap, this is optional and will be computed if not provided.
     * @return {object} an object {firstColumn: , lastColumn: , fristRow: , lastRow: } 
     */
    var visibleTilemapIndexes = function (element, elementOffset) {
        if (elementOffset === undefined) {
            elementOffset = offset(element);   
        }
        
        var gameQuery = element[0].gameQuery;
        // Activate the visible tiles
        return {
            firstRow:    Math.max(Math.min(Math.floor(-elementOffset.y/gameQuery.height), gameQuery.sizey), 0),
            lastRow:     Math.max(Math.min(Math.ceil(($.gameQuery.playground[0].height-elementOffset.y)/gameQuery.height), gameQuery.sizey), 0),
            firstColumn: Math.max(Math.min(Math.floor(-elementOffset.x/gameQuery.width), gameQuery.sizex), 0),
            lastColumn:  Math.max(Math.min(Math.ceil(($.gameQuery.playground[0].width-elementOffset.x)/gameQuery.width), gameQuery.sizex), 0) 
        }
    }
    
    /**
     * Utility function thast computes the buffered zone of a tilemap
     * 
     * @param {jQuery} element the jQuery wrapped DOM element representing the tilemap.
     * @param {object} visible an object describing the visible zone
     * @return {object} an object {firstColumn: , lastColumn: , fristRow: , lastRow: }
     */
    var bufferedTilemapIndexes = function (element, visible) {
        var gameQuery = element[0].gameQuery;
        
        return {
            firstRow:    Math.max(Math.min(visible.firstRow - gameQuery.buffer, gameQuery.sizey), 0),
            lastRow:     Math.max(Math.min(visible.lastRow + gameQuery.buffer, gameQuery.sizey), 0),
            firstColumn: Math.max(Math.min(visible.firstColumn - gameQuery.buffer, gameQuery.sizex), 0),
            lastColumn:  Math.max(Math.min(visible.lastColumn + gameQuery.buffer, gameQuery.sizex), 0) 
        }
    }
    
    /**
     * Utility function that creates a tile in the given tilemap
     * 
     * @param {jQuery} tileSet the jQuery element representing the tile map
     * @param {integer} row the row index of the tile in the tile map
     * @param {integer} column the column index of the tile in the tile map
     */
    var addTile = function(tileSet, row, column) {
        var gameQuery = tileSet[0].gameQuery;
        var name = tileSet.attr("id");
        
        var tileDescription;
        if(gameQuery.func) {
            tileDescription = gameQuery.tiles(row,column)-1;
        } else {
            tileDescription = gameQuery.tiles[row][column]-1;
        }
        
        var animation;
        if(gameQuery.multi) {
            animation = gameQuery.animations;
        } else {
            animation = gameQuery.animations[tileDescription];
        }
        
        if(tileDescription >= 0){
            tileSet.addSprite($.gameQuery.tileIdPrefix+name+"_"+row+"_"+column,
                                  {width: gameQuery.width,
                                   height: gameQuery.height,
                                   posx: column*gameQuery.width,
                                   posy: row*gameQuery.height,
                                   animation: animation});
                                   
            var newTile = tileSet.find("#"+$.gameQuery.tileIdPrefix+name+"_"+row+"_"+column);
            if (gameQuery.multi) {
                newTile.setAnimation(tileDescription);
            } else {
                newTile[0].gameQuery.animationNumber = tileDescription;
            }
            newTile.removeClass($.gameQuery.spriteCssClass);
            newTile.addClass($.gameQuery.tileCssClass);
            newTile.addClass($.gameQuery.tileTypePrefix+tileDescription);
        }
    }
    
    // Define the list of object/function accessible through $.
    $.extend({ gameQuery: {
        /**
         * This is the Animation Object
         */
        Animation: function (options, imediateCallback) {
            // private default values
            var defaults = {
                imageURL:      "",
                numberOfFrame: 1,
                delta:         0,
                rate:          30,
                type:          0,
                distance:      0,
                offsetx:       0,
                offsety:       0
            };

            // options extends defaults
            options = $.extend(defaults, options);

            // "public" attributes:
            this.imageURL      = options.imageURL;      // The url of the image to be used as an animation or sprite
            this.numberOfFrame = options.numberOfFrame; // The number of frames to be displayed when playing the animation
            this.delta         = options.delta;         // The distance in pixels between two frames
            this.rate          = options.rate;          // The rate at which the frames change in miliseconds
            this.type          = options.type;          // The type of the animation.This is a bitwise OR of the properties.
            this.distance      = options.distance;      // The distance in pixels between two animations
            this.offsetx       = options.offsetx;       // The x coordinate where the first sprite begins
            this.offsety       = options.offsety;       // The y coordinate where the first sprite begins

            // Whenever a new animation is created we add it to the ResourceManager animation list
            $.gameQuery.resourceManager.addAnimation(this, imediateCallback);

            return true;
        },

        /**
         * "constants" for the different types of an animation
         */ 
        ANIMATION_VERTICAL:   1,  // Generated by a vertical offset of the background
        ANIMATION_HORIZONTAL: 2,  // Generated by a horizontal offset of the background
        ANIMATION_ONCE:       4,  // Played only once (else looping indefinitely)
        ANIMATION_CALLBACK:   8,  // A callback is exectued at the end of a cycle
        ANIMATION_MULTI:      16, // The image file contains many animations
        ANIMATION_PINGPONG:   32, // At the last frame of the animation it reverses (if used in conjunction with ONCE it will have no effect)

        // "constants" for the different type of geometry for a sprite
        GEOMETRY_RECTANGLE:   1,
        GEOMETRY_DISC:        2,

        // basic values
        refreshRate:          30,

        /**
         * An object to manage resource loading
         */
        resourceManager: {
            animations: [],    // List of animations / images used in the game
            sounds:     [],    // List of sounds used in the game
            callbacks:  [],    // List of the functions called at each refresh
            loadedAnimationsPointer: 0, // Keep track of the last loaded animation
            loadedSoundsPointer:    0, // Keep track of the last loaded sound

            /**
             * Load resources before starting the game.
             */
            preload: function() {
                // Start loading the images
                for (var i = this.animations.length-1 ; i >= this.loadedAnimationsPointer; i --){
                    this.animations[i].domO = new Image();
                    this.animations[i].domO.src = this.animations[i].imageURL;
                }

                // Start loading the sounds
                for (var i = this.sounds.length-1 ; i >= this.loadedSoundsPointer; i --){
                    this.sounds[i].load();
                }

                $.gameQuery.resourceManager.waitForResources();
            },

            /**
             * Wait for all the resources called for in preload() to finish loading.
             */
            waitForResources: function() {
                // Check the images
                var imageCount = 0;
                for(var i=this.loadedAnimationsPointer; i < this.animations.length; i++){
                    if(this.animations[i].domO.complete){
                        imageCount++;
                    }
                }
                // Check the sounds
                var soundCount = 0;
                for(var i=this.loadedSoundsPointer; i < this.sounds.length; i++){
                    var temp = this.sounds[i].ready();
                    if(temp){
                        soundCount++;
                    }
                }
                // Call the load callback with the current progress
                if($.gameQuery.resourceManager.loadCallback){
                    var percent = (imageCount + soundCount)/(this.animations.length + this.sounds.length - this.loadedAnimationsPointer - this.loadedSoundsPointer)*100;
                    $.gameQuery.resourceManager.loadCallback(percent);
                }
                if(imageCount + soundCount < (this.animations.length + this.sounds.length  - this.loadedAnimationsPointer - this.loadedSoundsPointer)){
                    imgWait=setTimeout(function () {
                        $.gameQuery.resourceManager.waitForResources();
                    }, 100);
                } else {
                    this.loadedAnimationsPointer = this.animations.length;
                    this.loadedSoundsPointer = this.sounds.length;
                    
                    // All the resources are loaded! We can now associate the animation's images to their corresponding sprites
                    $.gameQuery.scenegraph.children().each(function(){
                        // recursive call on the children:
                        $(this).children().each(arguments.callee);
                        // add the image as a background
                        if(this.gameQuery && this.gameQuery.animation){
                            $(this).css("background-image", "url("+this.gameQuery.animation.imageURL+")");
                            // we set the correct kind of repeat
                            if(this.gameQuery.animation.type & $.gameQuery.ANIMATION_VERTICAL) {
                                $(this).css("background-repeat", "repeat-x");
                            } else if(this.gameQuery.animation.type & $.gameQuery.ANIMATION_HORIZONTAL) {
                                $(this).css("background-repeat", "repeat-y");
                            } else {
                                $(this).css("background-repeat", "no-repeat");
                            }
                        }
                    });

                    // Launch the refresh loop
                    if($.gameQuery.state === STATE_NEW){
                        setInterval(function () {
                            $.gameQuery.resourceManager.refresh();
                        },($.gameQuery.refreshRate));
                    }
                    $.gameQuery.state = STATE_RUNNING;
                    if($.gameQuery.startCallback){
                        $.gameQuery.startCallback();
                    }
                    // Make the scenegraph visible
                    $.gameQuery.scenegraph.css("visibility","visible");
                }
            },

            /**
             * This function refresh a unique sprite here 'this' represent a dom object
             */
            refreshSprite: function() {
                // Check if 'this' is a gameQuery element
                if(this.gameQuery != undefined){
                    var gameQuery = this.gameQuery;
                    // Does 'this' has an animation ?
                    if(gameQuery.animation){
                        // Do we have anything to do?
                        if ( (gameQuery.idleCounter == gameQuery.animation.rate-1) && gameQuery.playing){

                            // Does 'this' loops?
                            if(gameQuery.animation.type & $.gameQuery.ANIMATION_ONCE){
                                if(gameQuery.currentFrame < gameQuery.animation.numberOfFrame-1){
                                    gameQuery.currentFrame += gameQuery.frameIncrement;
                                } else if(gameQuery.currentFrame == gameQuery.animation.numberOfFrame-1) {
                                    // Does 'this' has a callback ?
                                    if(gameQuery.animation.type & $.gameQuery.ANIMATION_CALLBACK){
                                        if($.isFunction(gameQuery.callback)){
                                            gameQuery.callback(this);
                                            gameQuery.callback = false;
                                        }
                                    }
                                }
                            } else {
                                if(gameQuery.animation.type & $.gameQuery.ANIMATION_PINGPONG){
                                    if(gameQuery.currentFrame == gameQuery.animation.numberOfFrame-1 && gameQuery.frameIncrement == 1) {
                                        gameQuery.frameIncrement = -1;
                                    } else if (gameQuery.currentFrame == 0 && gameQuery.frameIncrement == -1) {
                                        gameQuery.frameIncrement = 1;
                                    }
                                }

                                gameQuery.currentFrame = (gameQuery.currentFrame+gameQuery.frameIncrement)%gameQuery.animation.numberOfFrame;
                                if(gameQuery.currentFrame == 0){
                                    // Does 'this' has a callback ?
                                    if(gameQuery.animation.type & $.gameQuery.ANIMATION_CALLBACK){
                                        if($.isFunction(gameQuery.callback)){
                                            gameQuery.callback(this);
                                        }
                                    }
                                }
                            }
                            // Update the background
                            if((gameQuery.animation.type & $.gameQuery.ANIMATION_VERTICAL) && (gameQuery.animation.numberOfFrame > 1)){
                                if(gameQuery.multi){
                                    $(this).css("background-position",""+(-gameQuery.animation.offsetx-gameQuery.multi)+"px "+(-gameQuery.animation.offsety-gameQuery.animation.delta*gameQuery.currentFrame)+"px");
                                } else {
                                    $(this).css("background-position",""+(-gameQuery.animation.offsetx)+"px "+(-gameQuery.animation.offsety-gameQuery.animation.delta*gameQuery.currentFrame)+"px");
                                }
                            } else if((gameQuery.animation.type & $.gameQuery.ANIMATION_HORIZONTAL) && (gameQuery.animation.numberOfFrame > 1)) {
                                if(gameQuery.multi){
                                    $(this).css("background-position",""+(-gameQuery.animation.offsetx-gameQuery.animation.delta*gameQuery.currentFrame)+"px "+(-gameQuery.animation.offsety-gameQuery.multi)+"px");
                                } else {
                                    $(this).css("background-position",""+(-gameQuery.animation.offsetx-gameQuery.animation.delta*gameQuery.currentFrame)+"px "+(-gameQuery.animation.offsety)+"px");
                                }
                            }
                        }
                        gameQuery.idleCounter = (gameQuery.idleCounter+1)%gameQuery.animation.rate;
                    }
                }
                return true;
            },

            /**
             * This function refresh a unique tile-map, here 'this' represent a dom object
             */
            refreshTilemap: function() {
                // Check if 'this' is a gameQuery element
                if(this.gameQuery != undefined){
                    var gameQuery = this.gameQuery;
                    if($.isArray(gameQuery.frameTracker)){
                        for(var i=0; i<gameQuery.frameTracker.length; i++){
                            // Do we have anything to do?
                            if(gameQuery.idleCounter[i] == gameQuery.animations[i].rate-1){
                                // Does 'this' loops?
                                if(gameQuery.animations[i].type & $.gameQuery.ANIMATION_ONCE){
                                    if(gameQuery.frameTracker[i] < gameQuery.animations[i].numberOfFrame-1){
                                        gameQuery.frameTracker[i] += gameQuery.frameIncrement[i];
                                    }
                                } else {
                                    if(gameQuery.animations[i].type & $.gameQuery.ANIMATION_PINGPONG){
                                        if(gameQuery.frameTracker[i] == gameQuery.animations[i].numberOfFrame-1 && gameQuery.frameIncrement[i] == 1) {
                                            gameQuery.frameIncrement[i] = -1;
                                        } else if (gameQuery.frameTracker[i] == 0 && gameQuery.frameIncrement[i] == -1) {
                                            gameQuery.frameIncrement[i] = 1;
                                        }
                                    }
                                    gameQuery.frameTracker[i] = (gameQuery.frameTracker[i]+gameQuery.frameIncrement[i])%gameQuery.animations[i].numberOfFrame;
                                }
                            }
                            gameQuery.idleCounter[i] = (gameQuery.idleCounter[i]+1)%gameQuery.animations[i].rate;
                        }
                    } else {
                        // Do we have anything to do?
                        if(gameQuery.idleCounter == gameQuery.animations.rate-1){
                            // Does 'this' loops?
                            if(gameQuery.animations.type & $.gameQuery.ANIMATION_ONCE){
                                if(gameQuery.frameTracker < gameQuery.animations.numberOfFrame-1){
                                    gameQuery.frameTracker += gameQuery.frameIncrement;
                                }
                            } else {
                                if(gameQuery.animations.type & $.gameQuery.ANIMATION_PINGPONG){
                                    if(gameQuery.frameTracker == gameQuery.animations.numberOfFrame-1 && gameQuery.frameIncrement == 1) {
                                        gameQuery.frameIncrement = -1;
                                    } else if (gameQuery.frameTracker == 0 && gameQuery.frameIncrement == -1) {
                                        gameQuery.frameIncrement = 1;
                                    }
                                }
                                gameQuery.frameTracker = (gameQuery.frameTracker+gameQuery.frameIncrement)%gameQuery.animations.numberOfFrame;
                            }
                        }
                        gameQuery.idleCounter = (gameQuery.idleCounter+1)%gameQuery.animations.rate;
                    }


                    // Update the background of all active tiles
                    $(this).find("."+$.gameQuery.tileCssClass).each(function(){
                        if($.isArray(gameQuery.frameTracker)){
                            var animationNumber = this.gameQuery.animationNumber
                            if((gameQuery.animations[animationNumber].type & $.gameQuery.ANIMATION_VERTICAL) && (gameQuery.animations[animationNumber].numberOfFrame > 1)){
                                $(this).css("background-position",""+(-gameQuery.animations[animationNumber].offsetx)+"px "+(-gameQuery.animations[animationNumber].offsety-gameQuery.animations[animationNumber].delta*gameQuery.frameTracker[animationNumber])+"px");
                            } else if((gameQuery.animations[animationNumber].type & $.gameQuery.ANIMATION_HORIZONTAL) && (gameQuery.animations[animationNumber].numberOfFrame > 1)) {
                                $(this).css("background-position",""+(-gameQuery.animations[animationNumber].offsetx-gameQuery.animations[animationNumber].delta*gameQuery.frameTracker[animationNumber])+"px "+(-gameQuery.animations[animationNumber].offsety)+"px");
                            }
                        } else {
                            if((gameQuery.animations.type & $.gameQuery.ANIMATION_VERTICAL) && (gameQuery.animations.numberOfFrame > 1)){
                                $(this).css("background-position",""+(-gameQuery.animations.offsetx-this.gameQuery.multi)+"px "+(-gameQuery.animations.offsety-gameQuery.animations.delta*gameQuery.frameTracker)+"px");
                            } else if((gameQuery.animations.type & $.gameQuery.ANIMATION_HORIZONTAL)  && (gameQuery.animations.numberOfFrame > 1)) {
                                $(this).css("background-position",""+(-gameQuery.animations.offsetx-gameQuery.animations.delta*gameQuery.frameTracker)+"px "+(-gameQuery.animations.offsety-this.gameQuery.multi)+"px");
                            }
                        }
                    });
                }
                return true;
            },

            /**
             * Called periodically to refresh the state of the game.
             */
            refresh: function() {
                if($.gameQuery.state === STATE_RUNNING) {
                    $.gameQuery.playground.find("."+$.gameQuery.spriteCssClass).each(this.refreshSprite);
                    $.gameQuery.playground.find("."+$.gameQuery.tilemapCssClass).each(this.refreshTilemap);
                    var deadCallback= new Array();
                    for (var i = this.callbacks.length-1; i >= 0; i--){
                        if(this.callbacks[i].idleCounter == this.callbacks[i].rate-1){
                            var returnedValue = this.callbacks[i].fn();
                            if(typeof returnedValue == 'boolean'){
                                // If we have a boolean: 'true' means 'no more execution', 'false' means 'keep on executing'
                                if(returnedValue){
                                    deadCallback.push(i);
                                }
                            } else if(typeof returnedValue == 'number') {
                                // If we have a number it re-defines the time to the next call
                                this.callbacks[i].rate = Math.round(returnedValue/$.gameQuery.refreshRate);
                                this.callbacks[i].idleCounter = 0;
                            }
                        }
                        this.callbacks[i].idleCounter = (this.callbacks[i].idleCounter+1)%this.callbacks[i].rate;
                    }
                    for(var i = deadCallback.length-1; i >= 0; i--){
                        this.callbacks.splice(deadCallback[i],1);
                    }
                }
            },

            /**
             * Add an animation to the resource Manager 
             */
            addAnimation: function(animation, callback) {
                if($.inArray(animation,this.animations)<0){
                    //normalize the animation rate:
                    animation.rate = Math.round(animation.rate/$.gameQuery.refreshRate);
                    if(animation.rate==0){
                        animation.rate = 1;
                    }
                    this.animations.push(animation);
                    switch ($.gameQuery.state){
                        case STATE_NEW:
                        case STATE_PAUSED:
                            // Nothing to do for now 
                            break;
                        case STATE_RUNNING:
                            // immediatly load the animation and call the callback if any
                            this.animations[this.loadedAnimationsPointer].domO = new Image();
                            this.animations[this.loadedAnimationsPointer].domO.src = animation.imageURL;
                            if (callback !== undefined){
                                this.animations[this.loadedAnimationsPointer].domO.onload = callback;
                            }
                            this.loadedAnimationsPointer++;
                            break;
                    }
                }
            },
            
            /**
             * Add a sound to the resource Manager 
             */
            addSound: function(sound, callback){
                if($.inArray(sound,this.sounds)<0){
                    this.sounds.push(sound);
                    switch ($.gameQuery.state){
                        case STATE_NEW:
                        case STATE_PAUSED:
                            // Nothing to do for now 
                            break;
                        case STATE_RUNNING:
                            // immediatly load the sound and call the callback if any
                            sound.load();
                            // TODO callback....
                            this.loadedSoundsPointer++;
                            break;
                    }
                }
            },

            /**
             * Register a callback
             * 
             * @param {function} fn the callback
             * @param {integer} rate the rate in ms at which the callback should be called (should be a multiple of the playground rate or will be rounded) 
             */
            registerCallback: function(fn, rate){
                rate  = Math.round(rate/$.gameQuery.refreshRate);
                if(rate==0){
                    rate = 1;
                }
                this.callbacks.push({fn: fn, rate: rate, idleCounter: 0});
            },
            
            /**
             * Clear the animations and sounds 
             */
            clear: function(callbacksToo){
                this.animations  = [];
                this.loadedAnimationsPointer = 0;
                this.sounds = [];
                this.loadedSoundsPointer = 0;
                if(callbacksToo) {
                    this.callbacks = [];
                }
            }
        },

        /**
         * This is a single place to update the underlying data of sprites/groups/tiles after a position or dimesion modification.
         */ 
        update: function(descriptor, transformation) {
            // Did we really receive a descriptor or a jQuery object instead?
            if(!$.isPlainObject(descriptor)){
                // Then we must get real descriptor
                if(descriptor.length > 0){
                    var gameQuery = descriptor[0].gameQuery;
                } else {
                    var gameQuery = descriptor.gameQuery;
                }
            } else {
                var gameQuery = descriptor;
            }
            // If we couldn't find one we return
            if(!gameQuery) return;
            if(gameQuery.tileSet === true){
                // We have a tilemap 
                
                var visible = visibleTilemapIndexes(descriptor);
                var buffered = gameQuery.buffered;
                
                // Test what kind of transformation we have and react accordingly 
                for(property in transformation){
                    switch(property){
                        case "x":
                        
                            if(visible.lastColumn > buffered.lastColumn) {
                                
                                // Detach the tilemap
                                var parent = descriptor[0].parentNode;
                                var tilemap = descriptor.detach();
                                
                                var newBuffered = bufferedTilemapIndexes(descriptor, visible);
                                for(var i = gameQuery.buffered.firstRow; i < gameQuery.buffered.lastRow; i++){
                                    // Remove the newly invisible tiles
                                    for(var j = gameQuery.buffered.firstColumn; j < Math.min(newBuffered.firstColumn, gameQuery.buffered.lastColumn); j++) {
                                        tilemap.find("#"+$.gameQuery.tileIdPrefix+descriptor.attr("id")+"_"+i+"_"+j).remove();
                                    }
                                    // And add the newly visible tiles
                                    for(var j = Math.max(gameQuery.buffered.lastColumn,newBuffered.firstColumn); j < newBuffered.lastColumn ; j++) {
                                        addTile(tilemap,i,j);
                                    }
                                }
                                
                                gameQuery.buffered.firstColumn = newBuffered.firstColumn;
                                gameQuery.buffered.lastColumn  = newBuffered.lastColumn;
                                
                                // Attach the tilemap back
                                tilemap.appendTo(parent);
                                
                            }
                            
                            if(visible.firstColumn < buffered.firstColumn) {
                                
                                // Detach the tilemap
                                var parent = descriptor[0].parentNode;
                                var tilemap = descriptor.detach();
                                    
                                var newBuffered = bufferedTilemapIndexes(descriptor, visible);
                                for(var i = gameQuery.buffered.firstRow; i < gameQuery.buffered.lastRow; i++){
                                    // Remove the newly invisible tiles
                                    for(var j = Math.max(newBuffered.lastColumn,gameQuery.buffered.firstColumn); j < gameQuery.buffered.lastColumn ; j++) {
                                        tilemap.find("#"+$.gameQuery.tileIdPrefix+descriptor.attr("id")+"_"+i+"_"+j).remove();
                                    }
                                    // And add the newly visible tiles
                                    for(var j = newBuffered.firstColumn; j < Math.min(gameQuery.buffered.firstColumn,newBuffered.lastColumn); j++) {
                                        addTile(tilemap,i,j);
                                    }
                                }
                                
                                gameQuery.buffered.firstColumn = newBuffered.firstColumn;
                                gameQuery.buffered.lastColumn  = newBuffered.lastColumn;
                                
                                // Attach the tilemap back
                                tilemap.appendTo(parent);
                            }
                            break;
                            
                        case "y":
                        
                            if(visible.lastRow > buffered.lastRow) {
                                
                                // Detach the tilemap
                                var parent = descriptor[0].parentNode;
                                var tilemap = descriptor.detach();
                                
                                var newBuffered = bufferedTilemapIndexes(descriptor, visible);
                                for(var j = gameQuery.buffered.firstColumn; j < gameQuery.buffered.lastColumn ; j++) {
                                    // Remove the newly invisible tiles
                                    for(var i = gameQuery.buffered.firstRow; i < Math.min(newBuffered.firstRow, gameQuery.buffered.lastRow); i++){
                                        tilemap.find("#"+$.gameQuery.tileIdPrefix+descriptor.attr("id")+"_"+i+"_"+j).remove();
                                    }
                                    // And add the newly visible tiles
                                    for(var i = Math.max(gameQuery.buffered.lastRow, newBuffered.firstRow); i < newBuffered.lastRow; i++){
                                        addTile(tilemap,i,j);
                                    }
                                }
                                
                                gameQuery.buffered.firstRow = newBuffered.firstRow;
                                gameQuery.buffered.lastRow  = newBuffered.lastRow;
                                
                                // Attach the tilemap back
                                tilemap.appendTo(parent);
                                
                            }  
                            
                            if(visible.firstRow < buffered.firstRow) {
                                
                                // Detach the tilemap
                                var parent = descriptor[0].parentNode;
                                var tilemap = descriptor.detach();
                                
                                var newBuffered = bufferedTilemapIndexes(descriptor, visible);
                                for(var j = gameQuery.buffered.firstColumn; j < gameQuery.buffered.lastColumn ; j++) {
                                    // Remove the newly invisible tiles
                                    for(var i = Math.max(newBuffered.lastRow, gameQuery.buffered.firstRow); i < gameQuery.buffered.lastRow; i++){
                                        tilemap.find("#"+$.gameQuery.tileIdPrefix+descriptor.attr("id")+"_"+i+"_"+j).remove();
                                    }
                                    // And add the newly visible tiles
                                    for(var i = newBuffered.firstRow; i < Math.min(gameQuery.buffered.firstRow, newBuffered.lastRow); i++){
                                        addTile(tilemap,i,j);
                                    }
                                }
                                
                                gameQuery.buffered.firstRow = newBuffered.firstRow;
                                gameQuery.buffered.lastRow  = newBuffered.lastRow;
                                
                                // Attach the tilemap back
                                tilemap.appendTo(parent);
                            }
                            break;
                            
                        case "angle":
                            //TODO
                            break;
                            
                        case "factor":
                            //TODO
                            break;
                    }
                }

            } else {
                var refreshBoundingCircle = $.gameQuery.playground && !$.gameQuery.playground.disableCollision;

                // Update the descriptor
                for(property in transformation){
                    switch(property){
                        case "x":
                            if(refreshBoundingCircle){
                                gameQuery.boundingCircle.x = gameQuery.posx+gameQuery.width/2;
                            }
                            break;
                        case "y":
                            if(refreshBoundingCircle){
                                gameQuery.boundingCircle.y = gameQuery.posy+gameQuery.height/2;
                            }
                            break;
                        case "w":
                        case "h":
                            gameQuery.boundingCircle.originalRadius = Math.sqrt(Math.pow(gameQuery.width,2) + Math.pow(gameQuery.height,2))/2
                            gameQuery.boundingCircle.radius = gameQuery.factor*gameQuery.boundingCircle.originalRadius;
                            break;
                        case "angle": //(in degrees)
                            gameQuery.angle = parseFloat(transformation.angle);
                            break;
                        case "factor":
                            gameQuery.factor = parseFloat(transformation.factor);
                            if(refreshBoundingCircle){
                                gameQuery.boundingCircle.radius = gameQuery.factor*gameQuery.boundingCircle.originalRadius;
                            }
                            break;
                    }
                }
            }
        },
        // State of the engine
        state: STATE_NEW,
        
        // CSS classes used to mark game element 
        spriteCssClass:  gQprefix + "sprite",
        groupCssClass:   gQprefix + "group",
        tilemapCssClass: gQprefix + "tilemap",
        tileCssClass:    gQprefix + "tile",
        // Prefix for CSS Ids or Classes
        tileTypePrefix:  gQprefix + "tileType_",
        tileIdPrefix:    gQprefix + "tile_"
    },

    /** 
     * Mute (or unmute) all the sounds.
     */
    muteSound: function(muted){
        for (var i = $.gameQuery.resourceManager.sounds.length-1 ; i >= 0; i --) {
            $.gameQuery.resourceManager.sounds[i].muted(muted);
        }
    },
    
    /**
     * Accessor for the currently defined playground as a jQuery object
     */
    playground: function() {
        return $.gameQuery.playground
    },
    
    /**
     * Define a callback called during the loading of the game's resources.
     *
     * The function will recieve as unique parameter
     * a number representing the progess percentage.
     */
    loadCallback: function(callback){
        $.gameQuery.resourceManager.loadCallback = callback;
    }
    }); // end of the extensio of $


    // fragments used to create DOM element
    var spriteFragment  = $("<div class='"+$.gameQuery.spriteCssClass+"'  style='position: absolute; display: block; overflow: hidden' />");
    var groupFragment   = $("<div class='"+$.gameQuery.groupCssClass+"'  style='position: absolute; display: block; overflow: hidden' />");
    var tilemapFragment = $("<div class='"+$.gameQuery.tilemapCssClass+"' style='position: absolute; display: block; overflow: hidden;' />");


    // Define the list of object/function accessible through $("selector").
    $.fn.extend({
        /**
         * Defines the currently selected div to which contains the game and initialize it.
         * 
         * This is a non-destructive call
         */
        playground: function(options) {
            if(this.length == 1){
                if(this[0] == document){ 
                    // Old usage detected, this is not supported anymore
                    throw "Old playground usage, use $.playground() to retreive the playground and $('mydiv').playground(options) to set the div!";
                }
                options = $.extend({
                    height:        320,
                    width:        480,
                    refreshRate: 30,
                    position:    "absolute",
                    keyTracker:    false,
                    mouseTracker: false,
                    disableCollision: false
                }, options);
                // We save the playground node and set some variable for this node:
                $.gameQuery.playground = this;
                $.gameQuery.refreshRate = options.refreshRate;
                $.gameQuery.playground[0].height = options.height;
                $.gameQuery.playground[0].width = options.width;

                // We initialize the display of the div
                $.gameQuery.playground.css({
                        position: options.position,
                        display:  "block",
                        overflow: "hidden",
                        height:   options.height+"px",
                        width:    options.width+"px"
                    })
                    .append("<div id='"+gQprefix+"scenegraph' style='visibility: hidden'/>");

                $.gameQuery.scenegraph = $("#"+gQprefix+"scenegraph");

                // Add the keyTracker to the gameQuery object:
                $.gameQuery.keyTracker = {};
                // We only enable the real tracking if the users wants it
                if(options.keyTracker){
                    $(document).keydown(function(event){
                        $.gameQuery.keyTracker[event.keyCode] = true;
                    });
                    $(document).keyup(function(event){
                        $.gameQuery.keyTracker[event.keyCode] = false;
                    });
                }
                
                // Add the mouseTracker to the gameQuery object:
                 $.gameQuery.mouseTracker = {
                    x: 0,
                    y: 0};
                // We only enable the real tracking if the users wants it
                var scenegraphOffset = $.gameQuery.playground.offset();
                if(options.mouseTracker){
                    $($.gameQuery.playground).mousemove(function(event){
                        $.gameQuery.mouseTracker.x = event.pageX - scenegraphOffset.left;
                        $.gameQuery.mouseTracker.y = event.pageY - scenegraphOffset.top;
                    });
                    $(document).mousedown(function(event){
                        $.gameQuery.mouseTracker[event.which] = true;
                    });
                    $(document).mouseup(function(event){
                        $.gameQuery.mouseTracker[event.which] = false;
                    });
                }
            }
            return this;
        },

        /**
         * Starts the game.
         *
         * Resources from the resource manager are preloaded if necesary
         * Works only for the playground node.
         *
         * This is a non-destructive call
         */
        startGame: function(callback) {
            $.gameQuery.startCallback = callback;
            $.gameQuery.resourceManager.preload();
            return this;
        },
        
        /**
         * TODO
         */
        pauseGame: function() {
            $.gameQuery.state = STATE_PAUSED;
            $.gameQuery.scenegraph.css("visibility","hidden");
            return this;
        },
        
        /**
         * Resume the game if it was paused and call the callback passed in argument once the newly added ressources are loaded.
         */
        resumeGame: function(callback) {
            if($.gameQuery.state === STATE_PAUSED){
                $.gameQuery.startCallback = callback;
                $.gameQuery.resourceManager.preload();
            }
            return this;
        },

        /**
         * Removes all the sprites, groups and tilemaps present in the scenegraph
         */
        clearScenegraph: function() {
            $.gameQuery.scenegraph.empty()
            return this;
        },
        
        /**
         * Removes all the sprites, groups and tilemaps present in the scenegraph as well as all loaded animations and sounds
         */
        clearAll: function(callbackToo) {
            $.gameQuery.scenegraph.empty();
            $.gameQuery.resourceManager.clear(callbackToo)
            return this;
        },

        /**
         * Add a group to the scene graph. Works only on the scenegraph root or on another group
         *
         * This IS a destructive call and should be terminated with end()
         * to go back one level up in the chaining
         */
        addGroup: function(group, options) {
            options = $.extend({
                width:      32,
                height:     32,
                posx:       0,
                posy:       0,
                posz:       0,
                posOffsetX: 0,
                posOffsetY: 0,
                overflow:   "visible",
                geometry:   $.gameQuery.GEOMETRY_RECTANGLE,
                angle:      0,
                factor:     1,
                factorh:    1,
                factorv:    1
            }, options);

            var newGroupElement = groupFragment.clone().attr("id",group).css({
                    overflow: options.overflow,
                    height:   options.height,
                    width:    options.width
                });
            
            if(this == $.gameQuery.playground){
                $.gameQuery.scenegraph.append(newGroupElement);
            } else if ((this == $.gameQuery.scenegraph)||(this.hasClass($.gameQuery.groupCssClass))){
                this.append(newGroupElement);
            }
            newGroupElement[0].gameQuery = options;
            newGroupElement[0].gameQuery.boundingCircle = {x: options.posx + options.width/2,
                                                    y: options.posy + options.height/0,
                                                    originalRadius: Math.sqrt(Math.pow(options.width,2) + Math.pow(options.height,2))/2};
            newGroupElement[0].gameQuery.boundingCircle.radius = newGroupElement[0].gameQuery.boundingCircle.originalRadius;
            newGroupElement[0].gameQuery.group = true;
            newGroupElement.transform();
            return this.pushStack(newGroupElement);
        },

        /**
         * Add a sprite to the current node. Works only on the playground or any of its sub-nodes 
         * 
         * This is a non-destructive call
         */
        addSprite: function(sprite, options) {
            options = $.extend({
                width:          32,
                height:         32,
                posx:           0,
                posy:           0,
                posz:           0,
                posOffsetX:     0,
                posOffsetY:     0,
                idleCounter:    0,
                currentFrame:   0,
                frameIncrement: 1,
                geometry:       $.gameQuery.GEOMETRY_RECTANGLE,
                angle:          0,
                factor:         1,
                playing:        true,
                factorh:        1,
                factorv:        1
            }, options);

            var newSpriteElem = spriteFragment.clone().attr("id",sprite).css({
                     height: options.height,
                     width: options.width,
                     backgroundPosition: ((options.animation)? -options.animation.offsetx : 0)+"px "+((options.animation)? -options.animation.offsety : 0)+"px"
                });
                
            if(this == $.gameQuery.playground){
                $.gameQuery.scenegraph.append(newSpriteElem);
            } else {
                this.append(newSpriteElem);
            }

            // If the game has already started we want to add the animation's image as a background now
            if(options.animation){
                // The second test is a fix for default background    (https://github.com/onaluf/gameQuery/issues/3)
                if($.gameQuery.state === STATE_RUNNING && options.animation.imageURL !== ''){
                    newSpriteElem.css("background-image", "url("+options.animation.imageURL+")");
                }
                if(options.animation.type & $.gameQuery.ANIMATION_VERTICAL) {
                    newSpriteElem.css("background-repeat", "repeat-x");
                } else if(options.animation.type & $.gameQuery.ANIMATION_HORIZONTAL) {
                    newSpriteElem.css("background-repeat", "repeat-y");
                } else {
                    newSpriteElem.css("background-repeat", "no-repeat");
                }
            }


            var spriteDOMObject = newSpriteElem[0];
            if(spriteDOMObject != undefined){
                spriteDOMObject.gameQuery = options;
                // Compute bounding Circle
                spriteDOMObject.gameQuery.boundingCircle = {x: options.posx + options.width/2,
                                                            y: options.posy + options.height/2,
                                                            originalRadius: Math.sqrt(Math.pow(options.width,2) + Math.pow(options.height,2))/2};
                spriteDOMObject.gameQuery.boundingCircle.radius = spriteDOMObject.gameQuery.boundingCircle.originalRadius;
            }
            newSpriteElem.transform();
            return this;
        },

        /**
         * Add a Tile Map to the selected element.
         *
         * This is a non-destructive call. The added sprite is NOT selected after a call to this function!
         */
        addTilemap: function(name, tileDescription, animationList, options){
            options = $.extend({
                width:          32,
                height:         32,
                sizex:          32,
                sizey:          32,
                posx:           0,
                posy:           0,
                posz:           0,
                posOffsetX:     0,
                posOffsetY:     0,
                angle:          0,
                factor:         1,
                factorh:        1,
                factorv:        1,
                buffer:         1
            }, options);

            var tileSet = tilemapFragment.clone().attr("id",name).css({
                    height: options.height*options.sizey, 
                    width: options.width*options.sizex
                });
            
            if(this == $.gameQuery.playground){
                $.gameQuery.scenegraph.append(tileSet);
            } else {
                this.append(tileSet);
            }
            
            tileSet[0].gameQuery = options;
            var gameQuery = tileSet[0].gameQuery;
            gameQuery.tileSet = true;
            gameQuery.tiles = tileDescription;
            gameQuery.func = (typeof tileDescription === "function");
                
            if($.isArray(animationList)){
                var frameTracker = [];
                var idleCounter = [];
                var frameIncrement = [];
                for(var i=0; i<animationList.length; i++){
                    frameTracker[i] = 0;
                    idleCounter[i] = 0;
                    frameIncrement[i] = 1;
                }
                gameQuery.frameTracker = frameTracker;
                gameQuery.animations = animationList;
                gameQuery.idleCounter =  idleCounter;
                gameQuery.frameIncrement = frameIncrement;
                gameQuery.multi = false;
            } else {
                gameQuery.frameTracker = 0;
                gameQuery.frameIncrement = 1;
                gameQuery.animations = animationList;
                gameQuery.idleCounter =  0;
                gameQuery.multi = true;
                
            }

            // Get the tileSet offset (relative to the playground)
            var visible = visibleTilemapIndexes(tileSet);
            var buffered = bufferedTilemapIndexes(tileSet, visible);
            gameQuery.buffered = buffered;

            // For many simple animation
            for(var i = buffered.firstRow; i < buffered.lastRow; i++){
                for(var j = buffered.firstColumn; j < buffered.lastColumn ; j++) {
                    addTile(tileSet, i, j);
                }
            }
            tileSet.transform()
            return this.pushStack(tileSet);
        },
        
        /**
         * This function imports a JSON file generated by Tiled (http://www.mapeditor.org/). 
         * All the created tilemaps will be directly under the currently selected element. 
         * Their name will be made of the provided prefix followed by a number starting at 0. 
         * 
         * Only layer of type "tilelayer" are supported for now. Only one single tileset 
         * per layer is supported.
         * 
         * After the call to this function the second argument will hold two new arrays:
         * - tiles: an arrays of tilemaps wraped in jQuery.
         * - animations: an arrays of animations 
         * 
         * This is a non-destructive call
         */
        importTilemaps: function(url, prefix, generatedElements){
            var animations = [];
            var tilemaps = [];
            
            var that = this;
            
            var tilemapJsonLoaded = function(json){
                var tilesetGID = [];
                for (var i = 0; i < json.tilesets.length; i++) {
                    tilesetGID[i] = json.tilesets[i].firstgid;
                } 
                
                var getTilesetIndex = function(index){
                    var i = 0;
                    while(index >= tilesetGID[i] && i < tilesetGID.length){
                        i++;
                    }
                    return i-1;
                }
        
                var height = json.height;
                var width  = json.width;
                var tileHeight = json.tileheight; 
                var tileWidth  = json.tilewidth;
                
                var layers = json.layers;
                var usedTiles = [];
                var animationCounter = 0;
                var tilemapArrays = [];
                
                // Detect which animations we need to generate
                // and convert the tiles array indexes to the new ones
                for (var i=0; i < layers.length; i++){
                    if(layers[i].type === "tilelayer"){
                        var tilemapArray = new Array(height);
                        for (var j=0; j<height; j++){
                            tilemapArray[j] = new Array(width);
                        }
                        for (var j=0; j < layers[i].data.length; j++){
                            var tile = layers[i].data[j];
                            if(tile === 0){
                                tilemapArray[Math.floor(j / width)][j % width] = 0;
                            } else {
                                if(!usedTiles[tile]){
                                    animationCounter++;
                                    usedTiles[tile] = animationCounter;
                                    animations.push(new $.gameQuery.Animation({
                                        imageURL: json.tilesets[getTilesetIndex(tile)].image,
                                        offsetx: ((tile-1) % Math.floor(json.tilesets[getTilesetIndex(tile)].imagewidth / tileWidth)) * tileWidth,
                                        offsety: Math.floor((tile-1) / Math.floor(json.tilesets[getTilesetIndex(tile)].imagewidth / tileWidth)) * tileHeight
                                    }));
                                }
                                tilemapArray[Math.floor(j / width)][j % width] = usedTiles[tile];
                            }
                        }
                        tilemapArrays.push(tilemapArray);
                    }
                }
                // adding the tilemaps
                for (var i=0; i<tilemapArrays.length; i++){
                     tilemaps.push(that.addTilemap(
                        prefix+i, 
                        tilemapArrays[i],
                        animations,
                        {
                            sizex:  width,
                            sizey:  height,
                            width:  tileWidth,
                            height: tileHeight
                    }));
                }
            };
    
            $.ajax({
                url: url,
                async: false,
                dataType: 'json',
                success: tilemapJsonLoaded
            });
            
            if(generatedElements !== undefined){
                generatedElements.animations = animations;
                generatedElements.tilemaps = tilemaps;
            }
        
            return this;
        },

        /**
         * Stop the animation at the current frame
         * 
         * This is a non-destructive call.
         */
        pauseAnimation: function() {
            this[0].gameQuery.playing = false;
            return this;
        },

        /**
         * Resume the animation (if paused)
         * 
         * This is a non-destructive call.
         */
        resumeAnimation: function() {
            this[0].gameQuery.playing = true;
            return this;
        },

        /**
         * Changes the animation associated with a sprite.
         *
         * WARNING: no checks are made to ensure that the object is really a sprite
         *
         * This is a non-destructive call.
         */
        setAnimation: function(animation, callback) {
            var gameQuery = this[0].gameQuery;
            if(typeof animation == "number"){
                if(gameQuery.animation.type & $.gameQuery.ANIMATION_MULTI){
                    var distance = gameQuery.animation.distance * animation;
                    gameQuery.multi = distance;
                    gameQuery.frameIncrement = 1;
                    gameQuery.currentFrame = 0;
                    
                    if(gameQuery.animation.type & $.gameQuery.ANIMATION_VERTICAL) {
                        this.css("background-position",""+(-distance-gameQuery.animation.offsetx)+"px "+(-gameQuery.animation.offsety)+"px");
                    } else if(gameQuery.animation.type & $.gameQuery.ANIMATION_HORIZONTAL) {
                        this.css("background-position",""+(-gameQuery.animation.offsetx)+"px "+(-distance-gameQuery.animation.offsety)+"px");
                    }
                }
            } else {
                if(animation){
                    gameQuery.animation = animation;
                    gameQuery.currentFrame = 0;
                    gameQuery.frameIncrement = 1;

                    if (animation.imageURL !== '') {this.css("backgroundImage", "url('"+animation.imageURL+"')");}
                    this.css({"background-position": ""+(-animation.offsetx)+"px "+(-animation.offsety)+"px"});

                    if(gameQuery.animation.type & $.gameQuery.ANIMATION_VERTICAL) {
                        this.css("background-repeat", "repeat-x");
                    } else if(gameQuery.animation.type & $.gameQuery.ANIMATION_HORIZONTAL) {
                        this.css("background-repeat", "repeat-y");
                    } else {
                        this.css("background-repeat", "no-repeat");
                    }
                } else {
                    this.css("background-image", "");
                }
            }

            if(callback != undefined){
                this[0].gameQuery.callback = callback;
            }

            return this;
        },

        /**
         * Register a callback funnction
         *
         * This is a non-destructive call
         *
         * @param {Function} fn the callback function.
         * @param {Number} rate time in milliseconds between calls.
         */
        registerCallback: function(fn, rate) {
            $.gameQuery.resourceManager.registerCallback(fn, rate);
            return this;
        },

        /**
         * Retrieve a list of objects in collision with the subject.
         *
         * If 'this' is a sprite or a group, the function will retrieve the list of sprites (not groups!!!) that touch it. For now all abject are considered to be boxes.
         *
         * This IS a destructive call and should be terminated with end() to go back one level up in the chaining.
         */
        collision: function(arg1, arg2){
            var filter, override;
            if ($.isPlainObject(arg1)){
                override = arg1;
            } else if (typeof arg1 === "string") {
                filter = arg1;
            }
            if ($.isPlainObject(arg2)){
                override = arg2;
            } else if (typeof arg2 === "string") {
                filter = arg2;
            }
            
            var resultList = [];

            // Retrieve 'this' offset by looking at the parents
            var itsParent = this[0].parentNode, offsetX = 0, offsetY = 0;
            while (itsParent != $.gameQuery.playground[0]){
                    if(itsParent.gameQuery){
                    offsetX += itsParent.gameQuery.posx;
                    offsetY += itsParent.gameQuery.posy;
                }
                itsParent = itsParent.parentNode;
            }

            // Retrieve the playground's absolute position and size information
            var pgdGeom = {top: 0, left: 0, bottom: $.playground().height(), right: $.playground().width()};

            // Retrieve the gameQuery object and correct it with the override
            var gameQuery = jQuery.extend(true, {}, this[0].gameQuery);

            // Retrieve the BoundingCircle and correct it with the override
            var boundingCircle = jQuery.extend(true, {}, gameQuery.boundingCircle);
            if(override && override.w){
                gameQuery.width = override.w;
            }
            if(override && override.h){
                gameQuery.height = override.h;
            }
            boundingCircle.originalRadius = Math.sqrt(Math.pow(gameQuery.width,2) + Math.pow(gameQuery.height,2))/2
            boundingCircle.radius = gameQuery.factor*boundingCircle.originalRadius;
            
            if(override && override.x){
                boundingCircle.x = override.x + gameQuery.width/2.0;
            }
            if(override && override.y){
                boundingCircle.y = override.y + gameQuery.height/2.0;
            }
            
            gameQuery.boundingCircle = boundingCircle;
            

            // Is 'this' inside the playground ?
            if( (gameQuery.boundingCircle.y + gameQuery.boundingCircle.radius + offsetY < pgdGeom.top)    ||
                (gameQuery.boundingCircle.x + gameQuery.boundingCircle.radius + offsetX < pgdGeom.left)   ||
                (gameQuery.boundingCircle.y - gameQuery.boundingCircle.radius + offsetY > pgdGeom.bottom) ||
                (gameQuery.boundingCircle.x - gameQuery.boundingCircle.radius + offsetX > pgdGeom.right)){
                return this.pushStack(new $([]));
            }

            if(this !== $.gameQuery.playground){
                // We must find all the elements that touche 'this'
                var elementsToCheck = new Array();
                elementsToCheck.push($.gameQuery.scenegraph.children(filter).get());
                elementsToCheck[0].offsetX = 0;
                elementsToCheck[0].offsetY = 0;

                for(var i = 0, len = elementsToCheck.length; i < len; i++) {
                    var subLen = elementsToCheck[i].length;
                    while(subLen--){
                        var elementToCheck = elementsToCheck[i][subLen];
                        // Is it a gameQuery generated element?
                        if(elementToCheck.gameQuery){
                            // We don't want to check groups
                            if(!elementToCheck.gameQuery.group && !elementToCheck.gameQuery.tileSet){
                                // Does it touche the selection?
                                if(this[0]!=elementToCheck){
                                    // Check bounding circle collision
                                    var distance = Math.sqrt(Math.pow(offsetY + gameQuery.boundingCircle.y - elementsToCheck[i].offsetY - elementToCheck.gameQuery.boundingCircle.y, 2) + Math.pow(offsetX + gameQuery.boundingCircle.x - elementsToCheck[i].offsetX - elementToCheck.gameQuery.boundingCircle.x, 2));
                                    if(distance - gameQuery.boundingCircle.radius - elementToCheck.gameQuery.boundingCircle.radius <= 0){
                                        // Check real collision
                                        if(collide(gameQuery, {x: offsetX, y: offsetY}, elementToCheck.gameQuery, {x: elementsToCheck[i].offsetX, y: elementsToCheck[i].offsetY})) {
                                            // Add to the result list if collision detected
                                            resultList.push(elementsToCheck[i][subLen]);
                                        }
                                    }
                                }
                            }
                            // Add the children nodes to the list
                            var eleChildren = $(elementToCheck).children(filter);
                            if(eleChildren.length){
                                elementsToCheck.push(eleChildren.get());
                                elementsToCheck[len].offsetX = elementToCheck.gameQuery.posx + elementsToCheck[i].offsetX;
                                elementsToCheck[len].offsetY = elementToCheck.gameQuery.posy + elementsToCheck[i].offsetY;
                                len++;
                            }
                        }
                    }
                }
                return this.pushStack($(resultList));
            }
        },

/** ---------------------------------------------------------------------------------------------------------------------------------------------------------------- **/
/** --          Sound related functions           ------------------------------------------------------------------------------------------------------------------ **/
/** ---------------------------------------------------------------------------------------------------------------------------------------------------------------- **/

        /**
         * Add the sound to the resourceManager for later use and
         * associates it to the selected dom element(s).
         * 
         * This is a non-destructive call
         */
        addSound: function(sound, add) {
            // Does a SoundWrapper exist?
            if($.gameQuery.SoundWrapper) {
                var gameQuery = this[0].gameQuery;
                // Should we add to existing sounds?
                if(add) {
                    // Do we have some sound associated with 'this'?
                    var sounds = gameQuery.sounds;
                    if(sounds) {
                        // Yes, we add it
                        sounds.push(sound);
                    } else {
                        // No, we create a new sound array
                        gameQuery.sounds = [sound];
                    }
                } else {
                    // No, we replace all sounds with this one
                    gameQuery.sounds = [sound];
                }
            }
            return this;
        },

        /**
         * Play the sound(s) associated with the selected dom element(s).
         * 
         * This is a non-destructive call.
         */
        playSound: function() {
            $(this).each(function(){
                var gameQuery = this.gameQuery;
                if(gameQuery.sounds) {
                    for(var i = gameQuery.sounds.length-1 ; i >= 0; i --) {
                        gameQuery.sounds[i].play();
                    }
                }
            });

            return this;
        },

        /**
         * Stops the sound(s) associated with the selected dom element(s) and rewinds them.
         * 
         * This is a non-destructive call.
         */
        stopSound: function() {
            $(this).each(function(){
                var gameQuery = this.gameQuery;
                if(gameQuery.sounds) {
                    for(var i = gameQuery.sounds.length-1 ; i >= 0; i --) {
                        gameQuery.sounds[i].stop();
                    }
                }
            });
            return this;
        },


        /**
         * Pauses the sound(s) associated with the selected dom element(s).
         * 
         * This is a non-destructive call.
         */

        pauseSound: function() {
            $(this).each(function(){
                var gameQuery = this.gameQuery;
                if(gameQuery.sounds) {
                    for(var i = gameQuery.sounds.length-1 ; i >= 0; i --) {
                        gameQuery.sounds[i].pause();
                    }
                }
            });
            return this;
        },


        /**
         * Mute or unmute the selected sound or all the sounds if none is specified.
         * 
         * This is a non-destructive call.
         */

        muteSound: function(muted) {
            $(this).each(function(){
                var gameQuery = this.gameQuery;
                if(gameQuery.sounds) {
                    for(var i = gameQuery.sounds.length-1 ; i >= 0; i --) {
                        gameQuery.sounds[i].muted(muted);
                    }
                }
            });
            return this;
        },


/** ---------------------------------------------------------------------------------------------------------------------------------------------------------------- **/
/** --          Transformation functions           ----------------------------------------------------------------------------------------------------------------- **/
/** ---------------------------------------------------------------------------------------------------------------------------------------------------------------- **/

        /**
         * Internal function doing the combined actions of rotate and scale. 
         * 
         * Please use .rotate() or .scale() instead since they are part of the supported API!
         * 
         * This is a non-destructive call.
         */
        transform: function() {
            var gameQuery = this[0].gameQuery;

			if(cssTransform){
				var transform = "translate("+gameQuery.posx+"px, "+gameQuery.posy+"px) rotate("+gameQuery.angle+"deg) scale("+(gameQuery.factor*gameQuery.factorh)+","+(gameQuery.factor*gameQuery.factorv)+")";
				this.css(cssTransform,transform);
			} else {
				var angle_rad = Math.PI * 2 / 360 * gameQuery.angle;
				// try filter for IE 
				// For ie from 5.5
                var cos = Math.cos(angle_rad) * gameQuery.factor;
                var sin = Math.sin(angle_rad) * gameQuery.factor;
                this.css("filter","progid:DXImageTransform.Microsoft.Matrix(M11="+(cos*gameQuery.factorh)+",M12="+(-sin*gameQuery.factorv)+",M21="+(sin*gameQuery.factorh)+",M22="+(cos*gameQuery.factorv)+",SizingMethod='auto expand',FilterType='nearest neighbor')");
                var newWidth = this.width();
                var newHeight = this.height();
                gameQuery.posOffsetX = (newWidth-gameQuery.width)/2;
                gameQuery.posOffsetY = (newHeight-gameQuery.height)/2;

                this.css("left", ""+(gameQuery.posx-gameQuery.posOffsetX)+"px");
                this.css("top", ""+(gameQuery.posy-gameQuery.posOffsetY)+"px");
			}
			
            return this;
        },

        /**
         * Rotate the element(s) clock-wise.
         *
         * @param {Number} angle the angle in degrees
         * @param {Boolean} relative or not
         *
         * This is a non-destructive call when called with a parameter. Without parameter it IS a destructive call since the return value is the current rotation angle!
         */
        rotate: function(angle, relative){
             var gameQuery = this[0].gameQuery;
 
             if(angle !== undefined) {
             	 if(relative === true){
                    angle += gameQuery.angle;
                    angle %= 360;
            	 }
                 $.gameQuery.update(gameQuery,{angle: angle});
                 return this.transform();
             } else {
                 var ang = gameQuery.angle;
                 return ang;
             }
        },

        /**
         * Change the scale of the selected element(s). The passed argument is a ratio:
         *
         * @param {Number} factor a ratio: 1.0 = original size, 0.5 = half the original size etc.
         * @param {Boolean} relative or not
         * 
         * This is a non-destructive call when called with a parameter. Without parameter it IS a destructive call since the return value is the current scale factor!
         */
        scale: function(factor, relative){
             var gameQuery = this[0].gameQuery;
 
             if(factor !== undefined) {
             	if(relative === true){
                    factor *= gameQuery.factor;
            	 }
                 $.gameQuery.update(gameQuery,{factor: factor});
                 return this.transform();
             } else {
                 var fac = gameQuery.factor;
                 return fac;
             }
        },

        /**
         * Flips the element(s) horizontally.
         * 
         * This is a non-destructive call when called with a parameter. Without parameter it IS a destructive call since the return value is the current horizontal flipping status!
         */
        fliph: function(flip){
            var gameQuery = this[0].gameQuery;

            if (flip === undefined) {
                return (gameQuery.factorh !== undefined) ? (gameQuery.factorh === -1) : false;
            } else if (flip) {
                gameQuery.factorh = -1;
            } else {
                gameQuery.factorh = 1;
            }

            return this.transform();
        },

        /**
         * Flips the element(s) vertically.
         * 
         * This is a non-destructive call when called with a parameter. Without parameter it IS a destructive call since the return value is the current vertical flipping status!
         */
        flipv: function(flip){
            var gameQuery = this[0].gameQuery;

            if (flip === undefined) {
                return (gameQuery.factorv !== undefined) ? (gameQuery.factorv === -1) : false;;
            } else if (flip) {
                gameQuery.factorv = -1;
            } else {
                gameQuery.factorv = 1;
            }

            return this.transform();
        },

/** ---------------------------------------------------------------------------------------------------------------------------------------------------------------- **/
/** --          Position getter/setter functions           --------------------------------------------------------------------------------------------------------- **/
/** ---------------------------------------------------------------------------------------------------------------------------------------------------------------- **/

        /**
         * Main function to change the sprite/group/tilemap position on screen.
         * The three first agruments are the coordiate (double) and the last one is a flag
         * to specify if the coordinate given are absolute or relative.
         *
         * If no argument is specified then the functions act as a getter and return a
         * object {x,y,z}
         *
         * Please note that the z coordinate is just the z-index.
         * 
         * This is a non-destructive call when called with a parameter. Without parameter it IS a destructive call.
         */
        xyz: function(x, y, z, relative) {
             if (x === undefined) {
                 return this.getxyz();
             } else {
                 return this.setxyz({x: x, y: y, z: z}, relative);
             }
        },

        /**
         * The following functions are all all shortcuts for the .xyz(...) function.
         *
         * @see xyz for detailed documentation.
         * 
         * This is a non-destructive call when called with a parameter. Without parameter it IS a destructive call.
         */
        x: function(value, relative) {
             if (value === undefined) {
                 return this.getxyz().x;
             } else {
                 return this.setxyz({x: value}, relative);
             }
        },

        y: function(value, relative) {
             if (value === undefined) {
                 return this.getxyz().y;
             } else {
                 return this.setxyz({y: value}, relative);
             }
        },

        z: function(value, relative) {
             if (value === undefined) {
                 return this.getxyz().z;
             } else {
                 return this.setxyz({z: value}, relative);
             }
        },

        xy: function(x, y, relative) {
             if (x === undefined) {
                 // we return the z too since it doesn't cost anything
                 return this.getxyz();
             } else {
                 return this.setxyz({x: x, y: y}, relative);
             }
        },

        /**
         * Main function to change the sprite/group/tilemap dimension on screen.
         * The two first arguments are the width and height (double) and the last one is a
         * flag to specify if the dimensions given are absolute or relative.
         *
         * If no argument is specified then the functions act as a getter and
         *
         * return an object {w,h}
         * 
         * This is a non-destructive call when called with a parameter. Without parameter it IS a destructive call.
         */
        wh: function(w, h, relative) {
            if (w === undefined) {
                 return this.getwh();
             } else {
                 return this.setwh({w: w, h: h}, relative);
             }
        },

        /**
         * The following functions are all all shortcuts for the .wh(...) function.
         *
         * @see wh for detailed documentation.
         * 
         * This is a non-destructive call when called with a parameter. Without parameter it IS a destructive call.
         */
        w: function(value, relative) {
            if (value === undefined) {
                 return this.getwh().w;
             } else {
                 return this.setwh({w: value}, relative);
             }
        },

        h: function(value, relative) {
            if (value === undefined) {
                 return this.getwh().h;
             } else {
                 return this.setwh({h: value}, relative);
             }
        },

        /**
         * The following four functions are 'private', and are not supposed to
         * be used outside of the library.
         * They are NOT part of the API and so are not guaranteed to remain unchanged.
         * You should really use .xyz() and .wh() instead.
         */
        getxyz: function() {
            var gameQuery = this[0].gameQuery;
            return {x: gameQuery.posx, y: gameQuery.posy, z: gameQuery.posz};
        },

        setxyz: function(option, relative) {
            var gameQuery = this[0].gameQuery;

            for (coordinate in option) {
                // Update the gameQuery object
                switch (coordinate) {
                    case 'x':
                        if(relative) {
                            option.x += gameQuery.posx;
                        }
                        gameQuery.posx = option.x;
                        this.transform();
                        
                        //update the sub tile maps (if any), this forces to recompute which tiles are visible
                        this.find("."+$.gameQuery.tilemapCssClass).each(function(){
                            $(this).x(0, true);
                        });
                        break;

                    case 'y':
                        if(relative) {
                            option.y += gameQuery.posy;
                        }
                        gameQuery.posy = option.y;
                        this.transform();
                        
                        //update the sub tile maps (if any), this forces to recompute which tiles are visible
                        this.find("."+$.gameQuery.tilemapCssClass).each(function(){
                            $(this).y(0, true);
                        });
                        break;

                    case 'z':
                        if(relative) {
                            option.z += gameQuery.posz;
                        }
                        gameQuery.posz = option.z;
                        this.css("z-index",gameQuery.posz);
                        break;
                }
            }
            $.gameQuery.update(this, option);
            return this;
        },

        getwh: function() {
            var gameQuery = this[0].gameQuery;
            return {w: gameQuery.width, h: gameQuery.height};
        },

        setwh: function(option, relative) {
            var gameQuery = this[0].gameQuery;

            for (coordinate in option) {
                // Update the gameQuery object
                switch (coordinate) {
                    case 'w':
                        if(relative) {
                            option.w += gameQuery.width;
                        }
                        gameQuery.width = option.w;
                        this.css("width","" + gameQuery.width + "px");
                        break;

                    case 'h':
                        if(relative) {
                            option.h += gameQuery.height;
                        }
                        gameQuery.height = option.h;
                        this.css("height","" + gameQuery.height + "px");
                        break;
                }
            }
            $.gameQuery.update(this, option);
            return this;
        }
    }); // end of the extensio of $.fn

    // alias gameQuery to gQ for easier access
    $.extend({ gQ: $.gameQuery}); 
})(jQuery);