var ballAnimator = function() {
    
    /**
     * Scaling factor between Box2D units and pixels.
     * 1m = 30px
     */
    var SCALE = 30;
    
    /**
     * Number of balls representing projects visible at any given time.
     */
    var PROJECT_COUNT = 8;
    
    /**
     * Minimum size of the project ball, in px.
     */
    var MIN_BALL_RADIUS = 75;
    
    /**
     * Maximum size of the project ball, in px.
     */
    var MAX_BALL_RADIUS = 100;
    
    /**
     * Vertical distance from top of ball to the top of its container
     */
    var BALL_TOP_OFFSET = 23;
    
    
    // shortcuts to Box2D namespace
    var b2AABB              = Box2D.Collision.b2AABB;
    var b2PolygonShape      = Box2D.Collision.Shapes.b2PolygonShape;
    var b2CircleShape       = Box2D.Collision.Shapes.b2CircleShape;
    
    var b2World             = Box2D.Dynamics.b2World;
    var b2FixtureDef        = Box2D.Dynamics.b2FixtureDef;
    var b2BodyDef           = Box2D.Dynamics.b2BodyDef;
    var b2Body              = Box2D.Dynamics.b2Body;
    var b2MouseJointDef     = Box2D.Dynamics.Joints.b2MouseJointDef;
    var b2ContactListener   = Box2D.Dynamics.b2ContactListener;
    
    var b2Vec2              = Box2D.Common.Math.b2Vec2;
    var b2Transform         = Box2D.Common.Math.b2Transform;
    var b2Mat22             = Box2D.Common.Math.b2Mat22;
    

    var stage = [window.screenX, window.screenY, window.innerWidth, window.innerHeight];
    getBrowserDimensions();
    
    var world;
    
    // Gravity changes
    var delta = [0, 0];
    
    var walls = [];
    var wallThickness = 200; // in px
    var wallsSet = false;
    
    // Logo has its own logic and doesn't belong to bodies
    var logoBody;
    
    var bodies = [];
    var activeBodies = [];
    var disabledBodies = [];
    
    
    // support for drag and drop
    var isMouseDown = false;
    var mouseJoint;
    var mouseX = 0;
    var mouseY = 0;
    
    // Desired framerate
    var timeStep = 1/20;
    
    // Flag indicating if collision can destroy balls.
    // Collisions happen very often, so it will be set to true only for some periods of time
    var allowBallCollision = false;
    
    function init() {
        
    	document.onmousedown = onMouseDown;
    	document.onmouseup = onMouseUp;
    	// jQuery is used to avoid errors in IE
    	$(document).mousemove(onMouseMove);
        
    	worldAABB = new b2AABB(); // init box2d
    	worldAABB.lowerBound.Set(-200/SCALE, -200/SCALE);
    	worldAABB.upperBound.Set((screen.width + 200)/SCALE, (screen.height + 200)/SCALE);
    	
    	world = new b2World( worldAABB, new b2Vec2( 0, 0 ), true );
    	
        setWalls();
        convertProjects();
        createLogoBall();
        selectActiveBalls();
        setCollisionHandler();
        
        play();
    }


    function play() {
        window.BallInterval = setInterval(loop, 1000/40);
        
        // The length of the interval decides for how long collisions cannot cause explosion.
        // When allowBallCollision is set to true, collision detection callback will become active.
        window.collisionTimeout = setInterval(function() {
            allowBallCollision = true;
        }, 5000);
        
    }
    
    
    /**
     * Iterate over active balls and move each one,
     * but only when their position and angle actually change, to save some CPU cycles.
     */
    function loop() {
        // Recalculate walls if browser window has been resized
    	if (getBrowserDimensions()) {
    		setWalls();
    	}
    	
    	delta[0] += (0 - delta[0]) * 0.5; // default .5
    	delta[1] += (0 - delta[1]) * 0.5;

    	world.m_gravity.x = 0 + delta[0];
    	world.m_gravity.y = 0 + delta[1];
    	
    	// Uncomment this to enable mouse drag
        mouseDrag();
    	
        world.Step(timeStep, 1, 1);
        
        // If logo has been moved, animate it. It's not in activeBodies, so has to be set in motion separately.
        animateBody(logoBody);
        
        // Animate project balls
        for (var i=0; i < activeBodies.length; i++) {
            animateBody(activeBodies[i], BALL_TOP_OFFSET);
        }
    }
    
    /**
     * Go through the list of projects and display each as an animated ball.
     */
    function convertProjects() {
        // make size of each ball container random
        var imgContainers = $("#content .projects li");
        var widthOffset, radius;
        var borderWidth;
        imgContainers.each(function(index) {
            var radius = Math.floor((Math.random() * (MAX_BALL_RADIUS - MIN_BALL_RADIUS) + MIN_BALL_RADIUS));
            var size = 2 * radius;
            var $element = $(this);
            var $circle = $element.find("img");
            $circle.attr("id", "body-" + index).width(size).height(size);
            
            var labelWidth = Math.floor($element.find("p").outerWidth());
            // ensure the tooltip has enough space
            widthOffset = Math.floor(radius/2);
            if (labelWidth > radius) {
                widthOffset += (labelWidth - radius);
            }
            // calculate it only once
            borderWidth = borderWidth || parseInt($circle.css("borderTopWidth"), 10);
            var containerWidth = size + widthOffset + borderWidth * 2;
            var containerHeight = size + BALL_TOP_OFFSET + borderWidth * 2;
            
            $element.css({
                "width": containerWidth,
                "height": containerHeight
            });
            
            createB2Circle(radius + borderWidth, $element, $circle);

        });
        
        $("#content .projects img").hover(function() {
            // show tooltip
            $(this).parents("li").css({"zIndex": 9999}).find("p").fadeIn(500);

            // Save movement information for future reference.
            // Can't refer to return value of GetLinearVelocity() directly, as it's an object.
            // Instead of copy of primitive values is made.            
            var index = parseInt($(this).attr("id").split("-")[1], 10);
            var body = bodies[index];
            body.GetDefinition().userData.linearVelocity = {
                x: body.GetLinearVelocity().x,
                y: body.GetLinearVelocity().y
            };
            body.GetDefinition().userData.angularVelocity = body.GetAngularVelocity();
            // freeze the body
            body.SetType(b2Body.b2_staticBody);
        }, function() {
            // hide the tooltip
            $(this).parents("li").css({"zIndex": 100}).find("p").fadeOut(500);
            // unfreeze the body
            var index = parseInt($(this).attr("id").split("-")[1], 10);
            var body = bodies[index];
            body.SetType(b2Body.b2_dynamicBody);
            
            // Restore old movement parameters to bring the body back to life
            body.SetLinearVelocity(body.GetDefinition().userData.linearVelocity);
            body.SetAngularVelocity(body.GetDefinition().userData.angularVelocity);
        });
    }
    
    /**
     * Create Box2D object representing the ball
     */
    function createB2Circle(radius, $element, $circle) {
    	var circleFixDef = new b2FixtureDef();
    	circleFixDef.shape = new b2CircleShape(radius/SCALE);
    	circleFixDef.density      = 0.1;
    	circleFixDef.restitution  = 0.9;
    	circleFixDef.friction     = 0.3;
    	
    	var b2bodyDef = new b2BodyDef();
    	b2bodyDef.type = b2Body.b2_dynamicBody;
        
        // Balls are created above the screen
    	var x = (Math.random() * stage[2]) / 4 + stage[2] / 2;
    	var y = Math.random() * -200;
        
        b2bodyDef.position.Set(x/SCALE, y/SCALE);
        
        b2bodyDef.userData = {
            $element: $element,
            $circle: $circle,
            type: "project",
            toBeDestroyed: false,
            oldPosition: {
                x: Math.floor(x),
                y: Math.floor(y),
                angle: 0
            }
        };
        b2bodyDef.linearVelocity.Set((Math.random() * 400)/SCALE, (Math.random() * 400 + 200)/SCALE);
    	var b2body = world.CreateBody(b2bodyDef);
    	b2body.CreateFixture(circleFixDef);
    	bodies.push(b2body);
    }
    
    
    /**
     * Create a box2D object representing the logo.
     */
    function createLogoBall() {
        var $logo = $("#logo");
        var radius = $logo.outerWidth()/2;
        
    	var circleFixDef = new b2FixtureDef();
    	circleFixDef.shape = new b2CircleShape(radius/SCALE);
    	circleFixDef.density      = 10.0;
    	circleFixDef.restitution  = 0.9;
    	circleFixDef.friction     = 0.3;
    	
    	var b2bodyDef = new b2BodyDef();
    	b2bodyDef.type = b2Body.b2_staticBody;
        
        // Initial position is taken from the logo image
        var x = $logo.offset().left + radius;
        var y = $logo.offset().top + radius;
        b2bodyDef.position.Set(x/SCALE, y/SCALE);
        
        b2bodyDef.userData = {
            $element: $logo,
            $circle: $logo,
            type: "logo",
            oldPosition: {
                x: Math.floor(x),
                y: Math.floor(y),
                angle: 0
            }
        };
    	logoBody = world.CreateBody(b2bodyDef);
    	logoBody.CreateFixture(circleFixDef);
    }
    
    /** Generic function to animate a body
     * @param   Object      Body to be animated
     * @param   Integer     Top offset required by project balls' tooltips; not needed for logo
     */
    function animateBody(body, circleTopOffset) {
        circleTopOffset = circleTopOffset || 0;
		var bodyDef = body.GetDefinition();
		var $element = bodyDef.userData.$element;
		var $circle = bodyDef.userData.$circle;
		var radius = Math.floor($circle.outerWidth()/2);
        
        var newPosition = {
            x: Math.floor(bodyDef.position.x * SCALE - radius),
            y: Math.floor(bodyDef.position.y * SCALE - radius),
            y: Math.floor(bodyDef.position.y * SCALE - circleTopOffset - radius),
            angle: body.GetAngle()
        };
        
        // Set angle
        if (bodyDef.userData.oldPosition.angle !== newPosition.angle) {
            // radians to degrees
			var rotationStyle = 'rotate(' + (newPosition.angle * 57.2957795) + 'deg)';
            $circle.css({
                "-webkit-transform": rotationStyle,
    			"-moz-transform": rotationStyle,
    			"-o-transform": rotationStyle,
                "-ms-transform": rotationStyle,
                "transform": rotationStyle
            });
            bodyDef.userData.oldPosition.angle = newPosition.angle;
        }
        
        // Store $element's style changes in one object,
        // so they can be applied in one batch
        var newStyle = {};
        if (bodyDef.userData.oldPosition.x !== newPosition.x) {
            newStyle.left = bodyDef.userData.oldPosition.x = newPosition.x;
        }
        
        if (bodyDef.userData.oldPosition.y !== newPosition.y) {
            newStyle.top = bodyDef.userData.oldPosition.y = newPosition.y;
        }
        if (newStyle !== {}) {
            $element.css(newStyle);
        }
    }

    /**
     * Split all bodies into two randomized sets:
     * activeBodies = balls that will be shown
     * disabledBodies = hidden balls that remain inactive above the viewport
     */
    function selectActiveBalls() {
        var randomIndices = [];
        for (var i = 0; i < bodies.length; i++) {
            randomIndices[i] = i;
        }
        randomIndices.sort(function() {
            return 0.5 - Math.random();
        });
        var activeIndices = randomIndices.slice(0, PROJECT_COUNT);
        var disabledIndices = randomIndices.slice(PROJECT_COUNT);
        for (i=0; i < activeIndices.length; i++) {
            activeBodies.push(bodies[activeIndices[i]]);
        }
        // Assemble list of disabledBodies and de-activate each one.
        for (i=0; i < disabledIndices.length; i++) {
            disabledBodies.push(bodies[disabledIndices[i]]);
            disabledBodies[disabledBodies.length - 1].SetActive(false);
        }
    }
    
    
    /**
     * Define event handler for collisions
     */
    function setCollisionHandler() {
        var contactListener = new b2ContactListener();
        contactListener.EndContact = function(contact) {
            // Don't do anything if collisions are not allowed at this moment
            if (!allowBallCollision) {
                return;
            }
            var bodyA = contact.GetFixtureA().GetBody();
            var bodyB = contact.GetFixtureB().GetBody();
            
            var slowerBody = bodyA.GetLinearVelocity().LengthSquared() < bodyB.GetLinearVelocity().LengthSquared() ? bodyA : bodyB;
            // Mark the ball to be destroyed and call the method to replace slower ball.
            // Make sure that frozen body (in hover state) won't be removed.
            if (slowerBody.GetDefinition().userData && slowerBody.GetDefinition().userData.type === "project"
                && slowerBody.GetType() === b2Body.b2_dynamicBody) {
                slowerBody.GetDefinition().userData.toBeDestroyed = true;
                replaceBall(slowerBody);
                allowBallCollision = false;
            }
        };
        
        world.SetContactListener(contactListener);
    }
    
    
    /**
     * Remove selected body and add a fresh one to the scene
     */
    function replaceBall(body) {    	
    	// make sure the tooltip doesn't appear
    	$element = body.GetUserData().$element;
    	$element.find('p').css({
    	    visibility: "hidden"
    	});
    	body.GetUserData().$circle.hide("puff", {percent: 250, origin: ["middle", "left"]}, 150, function() {
    	    removeBall();
    	    addBall();
    	});
    }
    
    /**
     * Remove marked ball from the scene
     */
	function removeBall() {
	    // Find the ball to be destroyed
	    for (var i=0, index=0, body; i < activeBodies.length; i++) {
	        if (activeBodies[i].GetDefinition().userData.toBeDestroyed === true) {
	            index = i;
	            body = activeBodies[i];
	            body.GetDefinition().userData.toBeDestroyed = false;
	            break;
	        }
	    };
        
        // Actually remove the ball
        if (body) {
    	    activeBodies.splice(index, 1);
    	    var x = (Math.random() * stage[2]) / 4 + stage[2] / 2;
        	var y = Math.random() * -200;
            body.SetTransform(new b2Transform(new b2Vec2(x/SCALE, y/SCALE), new b2Mat22(0)));
            body.SetActive(false);
            disabledBodies.push(body);
        }                
	}
	
	/**
	 * Add first of the previously disabled balls back the scene.
	 * Apply some initial impulse to it, so it has a dynamic entrance.
	 */
	function addBall() {
	    var body = disabledBodies.splice(0, 1)[0];
	    if (!body) {
	        return;
	    }
	    body.SetActive(true);
	    
        // make the ball visible again
    	var $element = body.GetUserData().$element;
    	$element.find('p').css({
    	    visibility: "visible"
    	});
        var $circle = body.GetUserData().$circle;
        $circle.show();
        var radius = Math.floor($circle.outerWidth()/2);
        body.GetUserData().$element.css({
            left: Math.floor(body.GetDefinition().position.x * SCALE - radius),
            top: Math.floor(body.GetDefinition().position.y * SCALE - BALL_TOP_OFFSET - radius * 2)
        });
        
        body.ApplyImpulse(new b2Vec2(Math.random() * 20, Math.random() * 20 + 10), body.GetDefinition().position);
	    activeBodies.push(body);

	}

    /**
     * Set the flag to allow mouse dragging
     */
    function onMouseDown() {
    	isMouseDown = true;
    	return false;
    }

    /**
     * Set the flag to disable mouse dragging mode
     */
    function onMouseUp() {
    	isMouseDown = false;
    	return false;
    }

    /**
     * Recalculate the position of mouse cursor
     */
    function onMouseMove(event) {
    	mouseX = event.clientX;
    	mouseY = event.clientY;
    }
    
    
    /**
     * Main definition of dragging behavior.
     */
    function mouseDrag() {
        if (isMouseDown && !mouseJoint) {
    		var body = getBodyAtMouse();
    		if (body) {
    		    body.SetType(b2Body.b2_dynamicBody);
    			var md = new b2MouseJointDef();
    			md.bodyA = world.GetGroundBody();
    			md.bodyB = body;
    			md.target.Set(mouseX/SCALE, mouseY/SCALE);
    			md.collideConnected = true;
    			md.maxForce = 1000 * body.GetMass();
    			md.timeStep = timeStep;
                mouseJoint = world.CreateJoint(md);
                body.SetAwake(true);
    		}
    	}

    	// mouse release
    	if (!isMouseDown) {
    		if (mouseJoint) {
    			world.DestroyJoint(mouseJoint);
    			mouseJoint = null;
    		}
    	}

    	// mouse move
    	if (mouseJoint) {
    		var p2 = new b2Vec2(mouseX/SCALE, mouseY/SCALE);
    		mouseJoint.SetTarget(p2);
    	}
    }

    /**
     * Return the body if it's being dragged.
     * Currently only logo ball is draggable.
     */
    function getBodyAtMouse() {
    	var mousePoint = new b2Vec2();
    	mousePoint.Set(mouseX/SCALE, mouseY/SCALE);

    	var aabb = new b2AABB();
    	aabb.lowerBound.Set((mouseX - 1)/SCALE, (mouseY - 1)/SCALE);
    	aabb.upperBound.Set((mouseX + 1)/SCALE, (mouseY + 1)/SCALE);

    	// Query the world for the body overlapping with the mouse click.
    	// Activate only the logo ball for dragging!
    	var body = null;
    	var callback = function(fixture) {
    	    if (fixture && fixture.GetBody() && fixture.GetBody().GetUserData().type === "logo") {
    	        body = fixture.GetBody();
    	        return true;
    	    }
    	    return false;
    	};
    	world.QueryPoint(callback, mousePoint);
    	
    	return body;
    }
    
    /**
     * Utility used to create walls
     */
    function createWall(x, y, width, height) {
    	var fixDef = new b2FixtureDef();
        fixDef.density = 1.0;
        fixDef.friction = 0.5;
        fixDef.restitution = 0.2;
        
        fixDef.shape = new b2PolygonShape();
        fixDef.shape.SetAsBox(width/SCALE, height/SCALE);

    	var bodyDef = new b2BodyDef();
    	bodyDef.type = b2Body.b2_staticBody;
    	bodyDef.position.Set(x/SCALE, y/SCALE);
    	
    	var body = world.CreateBody(bodyDef);
    	body.CreateFixture(fixDef);
    	
    	return body;
    }

    /**
     * Create or re-create the walls when browser window has been moved
     */
    function setWalls() {
    	if (wallsSet) {
    		world.DestroyBody(walls[0]);
    		world.DestroyBody(walls[1]);
    		world.DestroyBody(walls[2]);
    		world.DestroyBody(walls[3]);
    		walls[0] = null; 
    		walls[1] = null;
    		walls[2] = null;
    		walls[3] = null;
    	}

    	walls[0] = createWall(stage[2] / 2, - wallThickness, stage[2], wallThickness);
    	walls[1] = createWall(stage[2] / 2, stage[3] + wallThickness, stage[2], wallThickness);
    	walls[2] = createWall(- wallThickness, stage[3] / 2, wallThickness, stage[3]);
    	walls[3] = createWall(stage[2] + wallThickness, stage[3] / 2, wallThickness, stage[3]);
    	wallsSet = true;
    }
    
    /**
     * Update stage information. Used after window resize.
     */
    function getBrowserDimensions() {
    	var changed = false;
    	if (stage[0] != window.screenX) {
    		delta[0] = (window.screenX - stage[0]) * 1;
    		stage[0] = window.screenX;
    		changed = true;
    	}
    	if (stage[1] != window.screenY) {
    		delta[1] = (window.screenY - stage[1]) * 1;
    		stage[1] = window.screenY;
    		changed = true;
    	}
    	if (stage[2] != window.innerWidth) {
    		stage[2] = window.innerWidth;
    		changed = true;
    	}
    	if (stage[3] != window.innerHeight) {
    		stage[3] = window.innerHeight;
    		changed = true;
    	}
    	return changed;
    }


    // Use module pattern to return an object with one public method
    return {
        run: function() {
            init();
        }
    };
};

$(window).load(function() { // images must be loaded too, so DOMContentLoaded is too early
    var isCompatible = false;
    try {
        // Check if features required by Box2Dweb are in place.
        // Currently Box2Dweb is using __defineGetter__,
        // but it's possible that in the future it will move on to ECMAScript5 defineProperty syntax.
        if (Object.prototype.__defineGetter__ || Object.defineProperty({},"x",{get: function(){return true;}}).x) {
            isCompatible = true;
        }
    } catch (e) {};
    
    if (isCompatible) {
        LazyLoad.js("http://box2dweb.googlecode.com/svn/trunk/Box2d.min.js", function() {
            var animator = new ballAnimator();
            animator.run();
        });
    } else {
        $("body").addClass("static");
    }
});
