/**
*  Sesame Street Bubble Pop Game
*
*  @author William Malone (www.williammalone.com)
*/

/*global window, WM */

WM.bubblePop = function (GLOBAL, WM, options) {
	
	"use strict";
	
	var that = WM.game(GLOBAL, WM, options),
		hud,
		touches = [],
		catcher,
		currentRound = 0,
		numTargetRequired = 5,
		numTargetFound,
		labels,
		idleTimeoutAmount = 12000,
		lastUserInputTime,
		timeoutActive = false,
		percentEmpty = 0.3,
		percentTarget = 0.3,
		targetsNotPlayed = [],
		curTargetIndex,
		boundBottom = 240,
		curState,
		timeoutStart,
		background,
		bubbles,
		sidebar,
		setupOnce,
		payoffIndex,
		payoffInterval = 60 * 2,
		roundEndIndex,
		roundEndInterval = 60 * 1,
		minSpeedX = 0,
		minSpeedY = 0.9,
		maxSpeedX = 0.1,
		maxSpeedY = 1,
		catcherStartWidth,
		catcherStartHeight,
		roundsNotPlayed = [],
		lastRoundPlayed,
		numTargetBubblesPopped,
		
		//  Loads the resources for the round specified via parameter
		//
		loadRoundResources = function (roundIndex) {
			var i,
				s,
				specs = {};
		
			// Load background image
			if (options.rounds[roundIndex].background.filename) {				
				that.resourcer.add(options.rounds[roundIndex].background.filename);
				options.rounds[roundIndex].background.imageResource = that.resourcer.get(options.rounds[roundIndex].background.filename);
			} else {
				WM.error({
					name: "ResourceMissing", 
					message: "Background not specified in configuration file for round " + roundIndex + "."
				});	
			}
			
			// Load hud images
			specs.hud = options.hud;
			if (specs.hud) {
				specs.sidebar = options.hud.sidebar;
				// If a sidebar is specified
				if (specs.sidebar) {
					for (s = 0; s < specs.sidebar.states.length; s += 1) {
						that.resourcer.add(specs.sidebar.states[s].filename);
						specs.sidebar.states[s].imageResource = that.resourcer.get(specs.sidebar.states[s].filename);
					}
				}
			}		
			
			// Load catcher images
			specs.catcher = options.rounds[roundIndex].catcher;	
			for (s = 0; s < specs.catcher.states.length; s += 1) {
				that.resourcer.add(specs.catcher.states[s].filename);
				specs.catcher.states[s].imageResource = that.resourcer.get(specs.catcher.states[s].filename);
			}
			
			// Load bubble highlight image
			specs.bubbleHighlight = options.rounds[roundIndex].bubbleHighlight;
			that.resourcer.add(specs.bubbleHighlight.filename);
			specs.bubbleHighlight.imageResource = that.resourcer.get(specs.bubbleHighlight.filename);
			
			// Load bubble pop animation image
			specs.bubblePopAnimation = options.rounds[roundIndex].bubblePopAnimation;
			that.resourcer.add(specs.bubblePopAnimation.filename);
			specs.bubblePopAnimation.imageResource = that.resourcer.get(specs.bubblePopAnimation.filename);
			
			specs.target = options.rounds[roundIndex].target;	
			// Load target images
			specs.targetUnPopped = specs.target.unpopped;
			that.resourcer.add(specs.targetUnPopped.filename);
			specs.targetUnPopped.imageResource = that.resourcer.get(specs.targetUnPopped.filename);
			
			specs.targetPopped = specs.target.popped;
			that.resourcer.add(specs.targetPopped.filename);
			specs.targetPopped.imageResource = that.resourcer.get(specs.targetPopped.filename);
			
			// Load item label state images
			for (s = 0; s < specs.target.label.states.length; s += 1) {
				that.resourcer.add(specs.target.label.states[s].filename);
				specs.target.label.states[s].imageResource = that.resourcer.get(specs.target.label.states[s].filename);
			}
			
			// Load empty bubble image
			specs.emptyBubble = options.rounds[roundIndex].emptyBubble;
			that.resourcer.add(specs.emptyBubble.filename);
			specs.emptyBubble.imageResource = that.resourcer.get(specs.emptyBubble.filename);
		
			// Load distractors images
			specs.distractors = options.rounds[roundIndex].distractors;	
			for (i = 0; i < specs.distractors.length; i += 1) {
				
				that.resourcer.add(specs.distractors[i].filename);
				specs.distractors[i].imageResource = that.resourcer.get(specs.distractors[i].filename);

			}	
		},
		
		getNumCurrentTargets = function () {
			var i,
				numTargets = 0;
			// Check each bubble if it contains target
			for (i = 0; i < bubbles.length; i += 1) {
				if (bubbles[i].type === "TARGET") {
					numTargets += 1;
				}
			}
			
			return numTargets;
		},
		
		getNumCurrentEmptyBubbles = function () {
			var i,
				numEmpty = 0;
			// Check each bubble to see if it is empty
			for (i = 0; i < bubbles.length; i += 1) {
				if (bubbles[i].type === "EMPTY") {
					numEmpty += 1;
				}
			}
			
			return numEmpty;
		},
		
		getNumNonTargets = function () {
			var i,
				numNonTargets = 0;
			// Check each bubble if it contains target
			for (i = 0; i < bubbles.length; i += 1) {
				if (bubbles[i].type === "DISTRACTOR") {
					numNonTargets += 1;
				}
			}
			
			return numNonTargets;
		},
	
		// Create a new bubble and add it to the world
		//
		createBubble = function () {
			
			var i,
				j,
				bubblePos1,
				bubblePos2,
				distBtwBubbles,
				xPosOfGreatestDist,
				greatestDist,
				spaceForNewBubbleFound,
				bubbleOverlap,
				view,
				poppedSpec,
				poppedView,
				unPoppedView,
				unPoppedSpec,
				bubble,
				s,
				bubbleType,
				highlight, 
				popAnimation;
				
			// Set chance of bubble contents depending on if those contents are already on the screen
			if (getNumCurrentTargets() < 2) {
				bubbleType = "TARGET";
			} else if (getNumCurrentEmptyBubbles() === 0) {
				bubbleType = "EMPTY";
			} else if (getNumNonTargets() === 0) {
				bubbleType = "DISTRACTOR";
			} else {
			
				if (Math.random() < percentTarget) {
					bubbleType = "TARGET";
				} else {
					if (Math.random() < percentEmpty) {
						bubbleType = "EMPTY";
					} else {
						bubbleType = "DISTRACTOR";
					}
				}
			}

			// Define the spec based on the bubble type
			if (bubbleType === "EMPTY") {
				unPoppedSpec = options.rounds[currentRound].emptyBubble;
				poppedSpec = null;	
			} else if (bubbleType === "TARGET") {
				unPoppedSpec = options.rounds[currentRound].target.unpopped;
				poppedSpec = options.rounds[currentRound].target.popped;
			} else if (bubbleType === "DISTRACTOR") {
				unPoppedSpec = options.rounds[currentRound].distractors[parseInt(GLOBAL.Math.random() * options.rounds[currentRound].distractors.length)];
				poppedSpec = null;
			}
			
			// Create popped state
			if (poppedSpec !== null) {
				poppedView = WM.sprite(GLOBAL, WM, that.contexts.bubbles, poppedSpec);
			} else {
				poppedView = null;
			}

			// Create unpopped state
			unPoppedView = WM.sprite(GLOBAL, WM, that.contexts.bubbles, unPoppedSpec);
			
			// Create highlight
			highlight = WM.sprite(GLOBAL, WM, that.contexts.bubbles, options.rounds[currentRound].bubbleHighlight);
			
			// Create pop animation
			popAnimation = WM.sprite(GLOBAL, WM, that.contexts.bubbles, options.rounds[currentRound].bubblePopAnimation);
	
			// Create bubble
			bubble = WM.bubbleView(GLOBAL, WM, that.contexts.bubbles, unPoppedView, poppedView, highlight, popAnimation, {
				speedX: Math.random() * (maxSpeedX - minSpeedX) + minSpeedX,
				speedY: Math.random() * (maxSpeedY - minSpeedY) + minSpeedY
			});
			
			bubble.y = -(bubble.height + Math.random() * bubble.height);
			
			xPosOfGreatestDist = 0;
			greatestDist = 0;
			spaceForNewBubbleFound = false;
			
			// Choose a random x location for a limited number of times
			for (i = 0; i < 99; i += 1) {
			
				// Set the bubble to a random x location
				bubble.x = parseInt(Math.random() * (1024 - bubble.width - 100));
				bubblePos1 = {
					x: bubble.x + bubble.width / 2,
					y: bubble.y + bubble.width / 2
				};
				
				bubbleOverlap = false;
				
				// For each current bubble
				for (j = 0; j < bubbles.length; j += 1) {
				
					bubblePos2 = {
						x: bubbles[j].x + bubbles[j].width / 2,
						y: bubbles[j].y + bubbles[j].width / 2
					};
					
					distBtwBubbles = WM.math.vectors.dist(bubblePos1, bubblePos2);
					if (distBtwBubbles < bubble.width) {
						bubbleOverlap = true;
						
						if (distBtwBubbles > greatestDist) {
							xPosOfGreatestDist = bubble.x;
							greatestDist = distBtwBubbles;
						}
						//WM.debug("new bubble collision, try again. distBtwBubbles: " + distBtwBubbles.toFixed(2) + ", width: " + bubble.width + ", i: " + i + ", j: " + j + ", bubble.x: " + bubble.x);
						break;
					}
				}
				if (!bubbleOverlap) {
					spaceForNewBubbleFound = true;
					break;	
				}
			}
			if (!spaceForNewBubbleFound) {
				bubble.x = xPosOfGreatestDist;
			}

			// If no target is specified set it to the filename of the first state
			if (bubble.id === undefined) {
				bubble.id = unPoppedSpec.filename;
			}
			
			bubble.type = bubbleType;
			that.addView(bubble);
			bubbles.push(bubble);
		},
		
		roundComplete = function () {
		
			WM.logStats({
			    event: "round.end",
			    title: that.gameTitle,
			    id: that.gameId,
			    params : {
			    	round: currentRound
			    }
			});
			
			if (currentRound < options.rounds.length - 1) { 
				currentRound += 1;
			} else {
				currentRound = 0;
			}
			that.roundSuccess();
		},
		
		payoffSoundComplete = function () {
		
			if (that.audio) {
				that.audio.removeEventListener("COMPLETE", payoffSoundComplete);
			}
			curState = "START_PAYOFF";
	
		},
		
		removeBubble = function (bubble) {
		
			var i,
				bubbleIsTarget = Boolean(bubble.type === "TARGET");

			// Remove bubble from bubble array
			for (i = 0; i < bubbles.length; i += 1) {
				if (bubbles[i] === bubble) {
					bubbles.splice(i, 1);
					break;
				}
			}
			// Remove bubble from view list
			that.removeView(bubble);
			// Destory bubble
			bubble.destroy();

			// If target item
			if (bubbleIsTarget) {
				
				numTargetFound += 1;
				
				for (i = numTargetFound - 1; i < numTargetFound; i += 1) {
					labels[i].setState("Found");
				}
				
				if (numTargetFound >= numTargetRequired) {
				
					curState = "ANIMATE_SIDEBAR";
					sidebar.addEventListener("ANIMATION_COMPLETE", payoffSideBarAnimationPlayedOnce);
					sidebar.start();
					
					WM.message('that.playSound("PAYOFF")');
					
					if (that.playSound("PAYOFF")) {
						that.audio.addEventListener("COMPLETE", payoffSoundComplete);
					} else {
						payoffSoundComplete();
					}
					
				} else {
					if (that.playSound("CORRECT_HIT")) {
						that.audio.addEventListener("COMPLETE", that.playMusic);
					}
				}
			}
			
			// Create new bubble to take its place
			createBubble();
		},
		
		introAnimationComplete = function (point) {
			catcher.removeEventListener("ANIMATION_COMPLETE", introAnimationComplete);

			if (curState === "PLAY") {
				if (catcher.getState() === "WAVE") {
					catcher.setState("WAIT");
				}
			}
		},

		introSideBarAnimationComplete = function () {
			
			var i;
		
			sidebar.removeEventListener("ANIMATION_COMPLETE", introSideBarAnimationComplete);
			
			lastUserInputTime = + new Date();
			timeoutStart = + new Date();
			catcher.visible = true;
			catcher.addEventListener("ANIMATION_COMPLETE", introAnimationComplete);
			catcher.setState("WAVE");
			curState = "PLAY";
			
			bubbles = [];
			// Create bubbles
			for (i = 0; i < options.rounds[currentRound].numBubbles; i += 1) {
				GLOBAL.setTimeout(function () {
					createBubble();
				}, 300 * i);
			}
		},
		
		introSideBarAnimationPlayedOnce = function () {
		
			sidebar.removeEventListener("ANIMATION_COMPLETE", introSideBarAnimationPlayedOnce);
			
			sidebar.addEventListener("ANIMATION_COMPLETE", introSideBarAnimationComplete);
			sidebar.start();
		},
		
		payoffSideBarAnimationPlayedOnce = function () {
		
			if (that.audio && that.audio.enabled) {
				that.audio.removeEventListener("COMPLETE", payoffSoundComplete);
			}
		
			sidebar.removeEventListener("ANIMATION_COMPLETE", payoffSideBarAnimationPlayedOnce);
			sidebar.start();
		},
		
		introAudioComplete = function () {
		
			if (that.audio) {
				that.audio.removeEventListener("COMPLETE", introAudioComplete);
				that.playMusic();
			}
			
			sidebar.addEventListener("ANIMATION_COMPLETE", introSideBarAnimationPlayedOnce);
			sidebar.start();
		},
		
		checkForBubbleCollision = function (point) {
			
			var i,
				bubblePopped = false;
				
			// For each bubble
			for (i = 0; i < bubbles.length; i += 1) {
				
				// If point is inside the bubble
				if (bubbles[i].pointCollisionDetection(point)) {
					
					// If the bubble is not already popped
					if (bubbles[i].getState() !== "POP") {

						// If the bubble contains the target
						if (bubbles[i].type === "TARGET") {
						
							WM.logStats({
							    event: "answer.selected",
							    title: that.gameTitle,
							    id: that.gameId,
							    params : {
							    	object: bubbles[i].id,
							    	correct: true
							    }
							});
							
							// Drop the popped bubble
							bubbles[i].speedX = 0;
							bubbles[i].speedY = 10;
							
							//WM.debug("numTargetBubblesPopped: " + numTargetBubblesPopped);
							numTargetBubblesPopped += 1;
							
						// If the bubble does not contain the target
						} else {
						
							WM.logStats({
							    event: "answer.selected",
							    title: that.gameTitle,
							    id: that.gameId,
							    params : {
							    	object: bubbles[i].id,
							    	correct: false
							    }
							});
							
							// Remove the bubble once its been popped
							bubbles[i].addEventListener("POP_COMPLETE", function (bubble) {
								removeBubble(bubble);
							});
						}

						// Pop the bubble
						bubbles[i].setState("Pop");
					}
						
					// At least one bubble was popped
					bubblePopped = true;
				}
			}
	
			return bubblePopped;		
		};
	
	// Initialization
	//
	that.load = function () {
		
		var i;

		for (i = 0; i < options.rounds.length; i += 1) {
			loadRoundResources(i);
		}

		that.resourcer.addEventListener("LOAD_PROGRESS", that.onResourceLoaded);
		that.resourcer.addEventListener("RESOURCES_LOADED", that.onResourcesLoaded);
		that.resourcer.loadAll();	
	};
	
	that.setup = function () {
		
		var view,
			i,
			s,
			targetItemViews = [],
			targetItem,
			label,
			specs = {};
			
		// Choose a random target
		if (roundsNotPlayed.length === 0) {
			for (i = 0; i < options.rounds.length; i += 1) {
				roundsNotPlayed.push(i);
			}
			if (options.rounds.length > 1) {
				while (true) {
					roundsNotPlayed.sort(function () {
						return (Math.round(Math.random()) - 0.5);	
					});
					if (lastRoundPlayed === undefined || roundsNotPlayed[0] !== lastRoundPlayed) {
						break;
					}
				}
			}
		}
		
		// Set the current target to the first item in the array then remove that item
		currentRound = roundsNotPlayed[0];
		roundsNotPlayed.shift();
		lastRoundPlayed = currentRound;
		
		// Create background
		background = WM.sprite(GLOBAL, WM, that.contexts.background, options.rounds[currentRound].background);
		background.type = "BACKGROUND";
		background.offsetX = that.overallOffset.x;
		background.offsetY = that.overallOffset.y;
		that.addView(background);
		
		// Create catcher
		catcher = WM.actor(GLOBAL, WM, options.rounds[currentRound].catcher);
		for (s = 0; s < options.rounds[currentRound].catcher.states.length; s += 1) {
			options.rounds[currentRound].catcher.states[s].ctx = that.contexts.catcher;
			catcher.addState(options.rounds[currentRound].catcher.states[s]);
		}
		catcher.type = "CATCHER";
		catcherStartWidth = catcher.width;
		catcherStartHeight = catcher.height;
		catcher.visible = false;
		catcher.x = 1024 - catcherStartWidth;
        catcher.y = 768 - catcherStartHeight;
		that.addView(catcher);

		// Create hud
		if (options.hud) {
			// Create sidebar
			if (options.hud.sidebar) {
				sidebar = WM.actor(GLOBAL, WM, options.hud.sidebar);
				for (s = 0; s < options.hud.sidebar.states.length; s += 1) {
					options.hud.sidebar.states[s].ctx = that.contexts.hud;
					sidebar.addState(options.hud.sidebar.states[s]);
				}
				sidebar.type = "SIDEBAR";
				that.addView(sidebar);
			}
		}
		
		// Create Labels
		labels = [];
		specs.target = options.rounds[currentRound].target;
		//targetItem = options.rounds[currentRound].items[curTargetIndex];
		for (i = 0; i < numTargetRequired; i += 1) {
			label = WM.actor(GLOBAL, WM, specs.target.label);
			for (s = 0; s < specs.target.label.states.length; s += 1) {
				specs.target.label.states[s].ctx = that.contexts.hud;
				label.addState(specs.target.label.states[s]);
			}
			label.x = 10;
			label.y = (i * (label.height + 15)) + 73 + that.overallOffset.y;
			that.addView(label);
			labels.push(label);
		}
		
		if (that.audioEnabled) {
			that.audio.clearAllSounds();
			// Add game audio if it exists
			if (options.audio) {
				for (i = 0; i < options.audio.states.length; i += 1) {
					that.audio.setSound(options.audio.states[i]);
				}
			}
			// Add round audio if it exists
			if (options.rounds[currentRound].audio) {
				for (i = 0; i < options.rounds[currentRound].audio.states.length; i += 1) {
					that.audio.setSound(options.rounds[currentRound].audio.states[i]);
				}
			}
		}
		
		// Clear canvases
		that.clearCxt(that.contexts.transition);
		
		// Add events
		that.controller.addEventListener("TOUCH_START", that.touchStart);
		that.controller.addEventListener("TOUCH_MOVE", that.touchMove);
		that.controller.addEventListener("ORIENTATION_CHANGE", that.onOrientationChange);
		
		numTargetFound = 0;
		numTargetBubblesPopped = 0;
		roundEndIndex = 0;
		lastUserInputTime = + new Date();
		timeoutStart = + new Date();
		payoffIndex = 0;
		
		WM.logStats({
		    event: "round.start",
		    title: that.gameTitle,
		    id: that.gameId,
		    params : {
		    	round: currentRound
		    }
		});
		curState = "INTRO";
		if (that.playSound("INTRO")) {
			that.audio.addEventListener("COMPLETE", introAudioComplete);
		} else {
			introAudioComplete();
		}
	};
	
	//  Clear resources after round
	//
	that.breakdown = function () {
		
		var i,
			curView,
			viewList = [];
			
		for (i = 0; i < that.views.length; i += 1) {
			viewList.push(that.views[i])
		}
		
		for (i = 0; i < viewList.length; i += 1) {

			if (viewList[i].type !== "SUCCESS") {
				that.removeView(viewList[i]);
				viewList[i].destroy();
			}
		}
		bubbles = [];
		targetsNotPlayed.pop();
		
		that.clearCxt(that.contexts.hud);
		that.clearCxt(that.contexts.bubbles);
		that.clearCxt(that.contexts.catcher);
		
		that.controller.removeEventListener("TOUCH_START", that.touchStart);
		that.controller.removeEventListener("TOUCH_MOVE", that.touchMove);
		that.controller.removeEventListener("ORIENTATION_CHANGE", that.onOrientationChange);
	};
	
	//  Fired when device when the game canvas size is changed (e.g. on device rotation)
	//
	that.onCanvasSizeUpdated = function () {
		if (catcher) {
            catcher.x = 1024 - catcherStartWidth;
            catcher.y = 768 - catcherStartHeight;
		}
	}
	
	// Update
	//
	that.update = function () {
		
		var i,
			j,
			curView,
			lowestPoppedBubble,
			now = + new Date(),
			itemHighlighted = false,
			targetX,
			boundTop,
			distBtwBubbles,
			angleBtwBubbles,
			bubbleSpeed,
			bubblePos1,
			bubblePos2,
			bubbleCollide;
			
		if ((now - lastUserInputTime) > idleTimeoutAmount) {
			timeoutActive = true;
			lastUserInputTime = now;
			timeoutStart = now;
		}
		
		if (curState === "PLAY") {
		
			for (i = 0; i < bubbles.length; i += 1) {
			
				if (bubbles[i].getState() !== "POP") {
					
					bubbles[i].newX = bubbles[i].x + bubbles[i].speedX;
					bubbles[i].newY = bubbles[i].y + bubbles[i].speedY;
				}
			}
			
			for (i = 0; i < bubbles.length; i += 1) {
				
				// If bubble is popped
				if (bubbles[i].getState() === "POP") {
					
					// If the bubble is not empty
					if (bubbles[i].item !== null) {
					
						// If bubble has fallen off screen
						if (bubbles[i].y > 768) {
							
							removeBubble(bubbles[i]);
						
						// If bubble is still on the screen
						} else {
	
							// If the bubble contains the target item
							if (bubbles[i].type === "TARGET") {
								
								// Drop bubble a little
								bubbles[i].y += 10;
								
								// If any bubble is current popped
								if (lowestPoppedBubble !== undefined) {
									
									// Check if the current height
									if (bubbles[i].y > lowestPoppedBubble.y && bubbles[i].y < 768 - bubbles[i].height) {
										lowestPoppedBubble = bubbles[i];
									}
								} else {
									// If no other bubble is currently popped
									lowestPoppedBubble = bubbles[i];
								}
							// If the bubble does not contain the target
							} else {
								// If bubble is not empty
								if (bubbles[i].item) {
									if (bubbles[i].item.alpha - 0.1 <= 0) {
										bubbles[i].item.alpha = 0;	
										bubbles[i].item.dirty = true;
									} else {
										bubbles[i].item.alpha -= 0.1;
										bubbles[i].item.dirty = true;
									}
								}
							}
						}
					}
				} else { // If bubble is not popped
				
					if (timeoutActive && !itemHighlighted) {
						if (bubbles[i].type === "TARGET") {
						
							if (bubbles[i].getState() !== "HIGHLIGHT") {
								WM.logStats({
								    event: "timeout",
								    title: that.gameTitle,
								    id: that.gameId,
								    params : {
								    	description: "Highlight bubble",
								    	object: bubbles[i].id
								    }
								});
							}
							bubbles[i].setState("HIGHLIGHT");
							itemHighlighted = true;

						}
					} else {
						bubbles[i].setState("DEFAULT");
					}
					
					bubbleCollide = false;
					bubblePos1 = {
						x: bubbles[i].newX + bubbles[i].width / 2,
						y: bubbles[i].newY + bubbles[i].width / 2
					};
					
					if (bubbles[i].newY > 0) {

						for (j = 0; j < bubbles.length; j += 1) {
						
							bubblePos2 = {
								x: bubbles[j].newX + bubbles[j].width / 2,
								y: bubbles[j].newY + bubbles[j].width / 2
							}
							distBtwBubbles = WM.math.vectors.dist(bubblePos1, bubblePos2);
	
							if (bubbles[j].getState() !== "POP" && distBtwBubbles && distBtwBubbles < bubbles[i].width) {
			
								angleBtwBubbles = WM.math.getAngleFromTwoPoints(bubblePos1, bubblePos2);
								bubbleSpeed = WM.math.vectors.dist({
									x:0,
									y:0
								},{
									x:bubbles[i].speedX, 
									y:bubbles[i].speedY
								});
	
								bubbles[i].speedX = -bubbleSpeed * GLOBAL.Math.cos(angleBtwBubbles);
								bubbles[i].speedY = -bubbleSpeed * GLOBAL.Math.sin(angleBtwBubbles);
								
								bubbleCollide = true;
								break;
							}
						}
					}

					if (!bubbleCollide) { 
					
					    // Check if bubble reaches the exit button
						if (WM.math.vectors.dist({
							x: bubbles[i].newX + bubbles[i].width / 2,
							y: bubbles[i].newY + bubbles[i].width / 2
						}, {
							x: 1024 - 40,
							y: 40
						}) < bubbles[i].width / 2 + 40) {
							
							angleBtwBubbles = WM.math.getAngleFromTwoPoints(bubblePos1, {
								x: 1024 - 40,
								y: 40
							});
							bubbleSpeed = WM.math.vectors.dist({
								x:0,
								y:0
							},{
								x:bubbles[i].speedX, 
								y:bubbles[i].speedY
							});
							
							bubbles[i].speedX = -bubbleSpeed * GLOBAL.Math.cos(angleBtwBubbles);
							bubbles[i].speedY = -bubbleSpeed * GLOBAL.Math.sin(angleBtwBubbles);
							
							bubbleCollide = true;
						}
					}
					
					//if (!bubbleCollide) {
						// Set highest bound to the top of screen if highlighted
						//  or a bit higher if not highlighted
						//boundTop = (bubbles[i].getState() === "HIGHLIGHT") ? 0 : -bubbles[i].height;
						boundTop = 0;
						
						// Check if bubble reaches upper or lower bound
						if (bubbles[i].y > 768 - bubbles[i].height - boundBottom) {
							bubbles[i].speedY = -GLOBAL.Math.abs(bubbles[i].speedY);
						} else if (bubbles[i].y < boundTop) {
							bubbles[i].speedY = GLOBAL.Math.abs(bubbles[i].speedY);
						}
						// Check if bubble reaches left or right bound
						if (bubbles[i].x > 1024 - bubbles[i].width) {
							if (bubbles[i].speedX > 0) {
								bubbles[i].speedX = -GLOBAL.Math.abs(bubbles[i].speedX);
							}
						} else if (bubbles[i].x < 0) {
							if (bubbles[i].speedX < 0) {
								bubbles[i].speedX = GLOBAL.Math.abs(bubbles[i].speedX);
							}
						}
					//}
					
					//if (bubbles[i].y + bubbles[i].speedY < 0) {
					//	bubbles[i].speedY = minSpeedY;
					//}
					
					bubbles[i].x += bubbles[i].speedX;
					bubbles[i].y += bubbles[i].speedY;
					bubbles[i].dirty = true;
				}
			}
			
			//for (i = 0; i < bubbles.length; i += 1) {
			//	if (bubbles[i].x < 120 && bubbles[i].y < 20) {
			//		WM.debug("bubble pos: " + bubbles[i].x + ", " + bubbles[i].y);
			//	}
			//}
			
			// Determine if the catcher needs to be updated 
			
			// If a bubble is popped
			if (lowestPoppedBubble !== undefined) {
			
				// Move the catcher toward the popped bubble
				catcher.x += (lowestPoppedBubble.x - lowestPoppedBubble.width / 2 - catcher.x) / 2;

				// If the popped bubble is close to the catcher
				if (lowestPoppedBubble.y > (768 - boundBottom)) {
				
					// Remove the lowest bubble
					removeBubble(lowestPoppedBubble);
					
					// If the catcher is not jumping then jump
					if (catcher.getState() !== "JUMP") {
						catcher.addEventListener("ANIMATION_COMPLETE", function (view) {
							if (catcher.x < 0) {
								catcher.x = 0;
							}
							catcher.setState("Wait");
						});
						catcher.setState("JUMP");
					}
				} else {
					catcher.setState("MOVE");
				}
			}
		} else if (curState === "START_PAYOFF") {
		
			if (payoffIndex >= payoffInterval) {
				curState = "PAYOFF_COMPLETE"
			} else {
				payoffIndex += 1;
			}
			
		} else if (curState === "PAYOFF_COMPLETE") {
		
			roundEndIndex = 0;
			curState = "ROUND_END_TRANSITION";
			
		} else if (curState === "ROUND_END_TRANSITION") {
		
			if (roundEndIndex >= roundEndInterval) {
				roundComplete();
			} else {
				roundEndIndex += 1;
			}
		}
	
		// Update all views
		for (i = 0; i < that.views.length; i += 1) {
			that.views[i].update();
		}
	};
	
	that.render = function () {
		
		var i,
			bubbleDirty,
			hudDirty;
			
		// If any hud items need to be rendered
		for (i = 0; i < labels.length; i += 1) {
			if (labels[i].dirty) {
				hudDirty = true;
				break;
			}
		}
		if (sidebar.dirty) {
			hudDirty = true;
		}
		if (hudDirty) {
			// Mark all hud elements dirty
			for (i = 0; i < labels.length; i += 1) {
				labels[i].dirty = true;
			}
			sidebar.dirty = true;
			that.clearCxt(that.contexts.hud);
		}
		
		// If any bubbles need to be rendered
		if (bubbles) {
			for (i = 0; i < bubbles.length; i += 1) {
				if (bubbles[i].dirty) {
					bubbleDirty = true;
					break;
				}
			}
			if (bubbleDirty) {
				// Mark all bubbles dirty
				for (i = 0; i < bubbles.length; i += 1) {
					bubbles[i].dirty = true;
				}
				that.clearCxt(that.contexts.bubbles);
			}
		}
		
		if (catcher.dirty) {
			that.clearCxt(that.contexts.catcher);
		}
		
		if (curState === "ROUND_END_TRANSITION") {
			that.clearCxt(that.contexts.transition);
			that.contexts.transition.fillStyle = "rgba(255, 255, 255, " + (roundEndIndex / roundEndInterval) + ")";
			that.contexts.transition.fillRect(0, 0, that.viewportWidth, that.viewportHeight);
		}

		for (i = 0; i < that.views.length; i += 1) {
			that.views[i].render();
		}
	};
	
	// Fired when user touches the screen
	//
	that.touchStart = function (point) {
		
		var bubblePopped = false,
			now = + new Date();
			
		if (!that.paused) {
			
			lastUserInputTime = now;
			timeoutActive = false;
			
			if (curState === "PLAY") {
			
				if (numTargetBubblesPopped < numTargetRequired) {
			
					bubblePopped = checkForBubbleCollision(point);
					
					if (!bubblePopped) {
						WM.logStats({
						    event: "na.click",
						    title: that.gameTitle,
						    id: that.gameId,
						    params : {
						    	coordinates: point.x + ", " + point.y
						    }
						});
					}
				}
			}  else if (curState === "INTRO") {
				WM.logStats({
				    event: "vo.click",
				    title: that.gameTitle,
				    id: that.gameId,
				    params : {
				    	vo: "INTRO"
				    }
				});
			}
		}
	}
	
	that.touchMove = function (point) {
		
		var bubblePopped = false,
			now = + new Date();
		
		if (!that.paused && that.controller.dragging) {
			
			lastUserInputTime = now;	
			timeoutActive = false;
			
			if (curState === "PLAY") {
		
				if (numTargetBubblesPopped < numTargetRequired) {
					bubblePopped = checkForBubbleCollision(point);
				}
			}
		}
	};
	
	that.gameTitle = "Bubble Pop";
	that.gameId = "BubblePop";
	
	// Add game specific canvases
	that.addCanvas("hud");
	that.addCanvas("catcher");
	that.addCanvas("bubbles");
	that.addCanvas("transition");
	
	that.init();
	
	return that;
}

