/**********************************************************************
                           Wii Remote events
                     By Mark Wilton-Jones 30/7/2007
                              Version 1.0
***********************************************************************

Please see http://www.howtocreate.co.uk/jslibs/ for details and a demo of this script
Please see http://www.howtocreate.co.uk/jslibs/termsOfUse.html for terms of use

Allows event listeners to be added for Wii Remotes, to detect when they move.

To use:

To use this, inbetween the <head> tags, put:

	<script src="PATH TO SCRIPT/wiimoteevents.js" type="text/javascript"></script>

The script provides an object that can be told to listen for threshold and key
events created by detecting motion from the Wii Remotes. Methods provided:

window.opera.wiievents.setPolling(integer: n)
  Says how often to check for a change in motion or key status (in milliseconds).
  Set n to 0 to prevent polling. Can be called at any time to change the polling interval.
  Make sure the interval is not too small, or the script will not be able to run fast
  enough (10 ms is the absolute lowest limit). Make sure the interval is not too big,
  or the script may miss important movements or button presses. Find the best balance
  for your script.
window.opera.wiievents.monitorRemote(integer: remoteindex,boolean: startstop)
  Starts or stops monitoring of a specific Wii Remote.
  remoteindex must be an index between 0 and 3, corresponsing to the Wii Remote index.
  startstop should be true to start monitoring the remote, or false to stop monitoring.
window.opera.wiievents.addEventListener(string: type,function: handler,boolean: capture)
  Normal DOM events method that can add listeners for the following key events:
    keydown
    keypress
    keyup
  Key events are not fired for the isBrowsing remote (use normal key events).
  Key events cannot be cancelled, and are fired with the opera.wiievents object as the
  target. Event objects also have the properties keyCode (matches the bitmask for the
  button), detail (the index of the remote), and remoteData (the current KpadStatus object
  for the remote).
  The addEventListener method can also add listeners for the following motion threshold
  events:
    wiiroll-n.m (roll the remote)
    wiidistance-n.m (move the remote closer to, or further from, the sensor bar)
    wiimove-n.m (move the cursor)
    wiimovehoriz-n.m (move the cursor horizontally)
    wiimovevert-n.m (move the cursor vertically)
  In these cases, n.m is a floating point number representing a threshold that must be
  exceeded within the polling interval. In the case of move and distance events, the
  threshold means that the dpdX and/or dpdY values or dpdDistance value must change by
  at least that much within the polling interval. In the case of roll events, the rotation
  angle in radians must change by at least the specified amount within the polling interval.
  If a movement is enough to trigger several thresholds (such as a distance change of 0.3
  being enough to trigger both a 0.2 and 0.1 threshold), the largest will be triggered
  first, followed by the smaller thresholds. Prevent the default action of the event for
  the larger threshold to prevent the smaller one from triggering (using preventDefault).
  Event objects also have the properties difference (the change in the relevant property),
  detail (the index of the remote), and remoteData (the current KpadStatus object for the
  remote).
window.opera.wiievents.removeEventListener(string: type,function: handler,boolean: capture)
  Normal DOM events method that can remove event listeners.
________________________________________________________________________________*/

(function () {

	function orderByThreshold(a,b) {
		return b.threshold - a.threshold;
	}

	function createFakeEvent(evtype,remote,diff,ind) {
		//create an event object and give it the details it needs
		var evObj = document.createEvent('Events');
		evObj.initEvent( evtype, false, true );
		evObj.remoteData = remote;
		evObj.difference = diff;
		evObj.detail = ind;
		//fire the event on the element
		wiievents.dispatchEvent(evObj);
	}

	function createFakeKeyEvent(evtype,remote,keyCode,ind) {
		//create an event object and give it the details it needs
		var evObj = document.createEvent('UIEvents');
		evObj.initUIEvent( evtype, false, false, window, ind );
		evObj.remoteData = remote;
		evObj.keyCode = keyCode;
		//fire the event on the element
		wiievents.dispatchEvent(evObj);
	}

	function pollForChanges() {

		var tempValues = {}, tempRemote, tempkeys, difference, absdifference, n, numlist;
		for( var i = 0; i < maxRemotes; i++ ) {
			if( remotesToCheck[i] && ( tempRemote = wiiremote.update(i) ).isEnabled ) {
				//even if no events are being waited for, must still continue to collect data,
				//so that if a new event listener is added, it starts with valid data
				tempValues.roll = Math.atan2(tempRemote.dpdRollY,tempRemote.dpdRollX);
				tempValues.distance = tempRemote.dpdDistance;
				tempValues.vert = tempRemote.dpdY;
				tempValues.horiz = tempRemote.dpdX;
	
				//check for differences only if needed, and fire events
				if( evToCheck.rollEvents.numeric.length ) {
					difference = tempValues.roll - remotedatastore[i].roll;
					absdifference = Math.abs(difference);
					//roll wraps around
					if( absdifference > Math.PI ) {
						absdifference = ( 2 * Math.PI ) - absdifference;
						difference = ( ( ( difference > 0 ) ? -2 : 2 ) * Math.PI ) + difference;
					}
					numlist = evToCheck.rollEvents.numeric;
					for( n = 0; n < numlist.length; n++ ) {
						//for each threshold, if it has been exceeded, fire the event - if it is cancelled, do not check lower numbers
						if( absdifference >= numlist[n].threshold && !createFakeEvent(numlist[n].eventname,tempRemote,difference,i) ) {
							break;
						}
					}
				}
				//basically the same idea as roll, only without wraparound
				if( evToCheck.distanceEvents.numeric.length ) {
					difference = tempValues.distance - remotedatastore[i].distance;
					absdifference = Math.abs(difference);
					numlist = evToCheck.distanceEvents.numeric;
					for( n = 0; n < numlist.length; n++ ) {
						if( absdifference >= numlist[n].threshold && !createFakeEvent(numlist[n].eventname,tempRemote,difference,i) ) {
							break;
						}
					}
				}
				if( evToCheck.movehorizEvents.numeric.length ) {
					difference = tempValues.horiz - remotedatastore[i].horiz;
					absdifference = Math.abs(difference);
					numlist = evToCheck.movehorizEvents.numeric;
					for( n = 0; n < numlist.length; n++ ) {
						if( absdifference >= numlist[n].threshold && !createFakeEvent(numlist[n].eventname,tempRemote,difference,i) ) {
							break;
						}
					}
				}
				if( evToCheck.movevertEvents.numeric.length ) {
					difference = tempValues.vert - remotedatastore[i].vert;
					absdifference = Math.abs(difference);
					numlist = evToCheck.movevertEvents.numeric;
					for( n = 0; n < numlist.length; n++ ) {
						if( absdifference >= numlist[n].threshold && !createFakeEvent(numlist[n].eventname,tempRemote,difference,i) ) {
							break;
						}
					}
				}
				if( evToCheck.moveEvents.numeric.length ) {
					difference = Math.sqrt( Math.pow( tempValues.horiz - remotedatastore[i].horiz, 2 ) + Math.pow( tempValues.vert - remotedatastore[i].vert, 2 ) );
					absdifference = Math.abs(difference);
					numlist = evToCheck.moveEvents.numeric;
					for( n = 0; n < numlist.length; n++ ) {
						if( absdifference >= numlist[n].threshold && !createFakeEvent(numlist[n].eventname,tempRemote,difference,i) ) {
							break;
						}
					}
				}

				//store the values for next time
				remotedatastore[i].roll = tempValues.roll;
				remotedatastore[i].distance = tempValues.distance;
				remotedatastore[i].vert = tempValues.vert;
				remotedatastore[i].horiz = tempValues.horiz;

				//do key monitoring stuff
				for( n = 0; n < keyCodes.length; n++ ) {
					tempkeys = tempRemote.hold & keyCodes[n];
					if( tempkeys && !remotedatastore[i].keysdown[keyCodes[n]] ) {
						//fire keydown and keypress
						remotedatastore[i].keysdown[keyCodes[n]] = true;
						createFakeKeyEvent('keydown',tempRemote,keyCodes[n],i)
						createFakeKeyEvent('keypress',tempRemote,keyCodes[n],i)
					} else if( !tempkeys && remotedatastore[i].keysdown[keyCodes[n]] ) {
						//fire keyup
						remotedatastore[i].keysdown[keyCodes[n]] = false;
						createFakeKeyEvent('keyup',tempRemote,keyCodes[n],i)
					} else if( tempkeys ) {
						//fire keypress
						createFakeKeyEvent('keypress',tempRemote,keyCodes[n],i)
					}
				}
			}
		}

	}

	//this is only useful if the Wii Remote API is available
	if( !window.opera || !opera.wiiremote ) { return; }

	var maxRemotes = 4; //in case they add more later - you never know ;)

	//set up private variables
	var pollingInterval;
	var remotesToCheck = [];
	var remotedatastore = [];
	for( var i = 0; i < maxRemotes; i++ ) {
		remotedatastore[i] = { roll: 0, distance: 2.5, vert: -1, horiz: -1, keysdown: {} };
	}
	var evToCheck = { rollEvents: {numeric:[]}, distanceEvents: {numeric:[]}, moveEvents: {numeric:[]}, movevertEvents: {numeric:[]}, movehorizEvents: {numeric:[]} };
	var eventPattern = /wii(move(vert|horiz)?|roll|distance)-([\d\.]+)/;
	var keyCodes = [1,2,4,8,16,256,512,1024,2048,4096,8192,16384];
	var wiiremote = opera.wiiremote;

	//create the event listener interface by using a DOM Element node
	var wiievents = opera.wiievents = document.createElement('wiievents');
	//override listeners with custom methods
	wiievents.realAddEventListener = wiievents.addEventListener;
	wiievents.realRemoveEventListener = wiievents.addEventListener;
	wiievents.addEventListener = function (eventtype,handler,phase) {
		var captures;
		if( typeof(eventtype) == 'string' && ( captures = eventtype.match(eventPattern) ) ) {
			//store all the information needed about the event
			var eventbase = evToCheck[captures[1]+'Events'], thresh = captures[3];
			if( !eventbase[eventtype] ) {
				eventbase.numeric[eventbase.numeric.length] = eventbase[eventtype] = { threshold: thresh * 1, eventname: eventtype, count: 0, currentindex: 0 };
			}
			eventbase[eventtype].count++;
			eventbase.numeric.sort(orderByThreshold);
			for( var i = 0; i < eventbase.numeric.length; i++ ) {
				eventbase.numeric[i].currentindex = i;
			}
		}
		wiievents.realAddEventListener(eventtype,handler,phase);
	};
	wiievents.removeEventListener = function (eventtype,handler,phase) {
		var captures;
		if( typeof(eventtype) == 'string' && ( captures = eventtype.match(eventPattern) ) ) {
			//update all the information needed about the event
			var eventbase = evToCheck[captures[1]+'Events'], thresh = captures[3];
			if( !eventbase[eventtype] ) { return; }
			eventbase[eventtype].count--;
			if( !eventbase[eventtype].count ) {
				//nothing else seems to be listening for the event, so delete the object
				eventbase.numeric.splice(eventbase[eventtype].currentindex,1);
				for( var i = eventbase[eventtype].currentindex; i < eventbase.numeric.length; i++ ) {
					eventbase.numeric[i].currentindex = i;
				}
				delete eventbase[eventtype];
			}
		}
		wiievents.realAddEventListener(eventtype,handler,phase);
	};

	//provide methods for saying what remotes to check, and how often to check
	wiievents.setPolling = function (interval) {
		if( pollingInterval ) { clearInterval(pollingInterval); }
		if( interval ) {
			pollForChanges();
			pollingInterval = setInterval(pollForChanges,interval);
		}
	};
	wiievents.monitorRemote = function (remoteNum,enable) {
		remotesToCheck[remoteNum] = enable;
	};
})();