if (Garmin == undefined) var Garmin = {};
/**
 * @fileoverview Garmin.DeviceControl A high-level JavaScript API which supports listener and callback functionality.
 * 
 * @author Michael Bina michael.bina.at.garmin.com
 * @version 1.0
 */
/**
 * @class Garmin.DeviceControl
 * A controller object that can retrieve and send data to a Garmin 
 * device.<br><br>
 * 
 * The controller must be unlocked before anything can be done with it.  
 * Then you'll have to find a device before you can start to read data from
 * and write data to the device.<br><br>
 * 
 * We use the observer pattern (http://en.wikipedia.org/wiki/Observer_pattern)
 * to handle the asynchronous nature of device communication.  You must register
 * your class as a listener to this Object and then implement methods that will 
 * get called on certain events.<br><br>
 * 
 * Events:<br><br>
 *     onStartFindDevices called when starting to search for devices.
 *       the object returned is {controller: this}<br><br>
 *
 *     onCancelFindDevices is called when the controller is told to cancel finding
 *         devices {controller: this}<br><br>
 *
 *     onFinishFindDevices called when the devices are found.
 *       the object returned is {controller: this}<br><br>
 *
 *     onException is called when an exception occurs in a method
 *         object passed back is {msg: exception}<br><br>
 *
 *	   onInteractionWithNoDevice is called when the device is lazy loaded, but finds no devices,
 * 			yet still attempts a read/write action {controller: this}<br><br>
 * 
 *     onStartReadFromDevice is called when the controller is about to start
 *         reading from the device {controller: this}<br><br>
 * 
 *     onFinishReadFromDevice is called when the controller is done reading 
 *         the device.  the read is either a success or failure, which is 
 *         communicated via json.  object passed back contains 
 *         {success:this.garminPlugin.GpsTransferSucceeded, controller: this} <br><br>
 *
 *     onWaitingReadFromDevice is called when the controller is waiting for input
 *         from the user about the device.  object passed back contains: 
 *         {message: this.garminPlugin.MessageBoxXml, controller: this}<br><br>
 *
 *     onProgressReadFromDevice is called when the controller is still reading information
 *         from the device.  in this case the message is a percent complete/ 
 *         {progress: this.getDeviceStatus(), controller: this}<br><br>
 *
 *     onCancelReadFromDevice is called when the controller is told to cancel reading
 *         from the device {controller: this}<br><br>
 *
 *     onFinishWriteToDevice is called when the controller is done writing to 
 *         the device.  the write is either a success or failure, which is 
 *         communicated via json.  object passed back contains 
 *         {success:this.garminPlugin.GpsTransferSucceeded, controller: this}<br><br>
 *
 *     onWaitingWriteToDevice is called when the controller is waiting for input
 *         from the user about the device.  object passed back contains: 
 *         {message: this.garminPlugin.MessageBoxXml, controller: this}<br><br>
 *
 *     onProgressWriteToDevice is called when the controller is still writing information
 *         to the device.  in this case the message is a percent complete/ 
 *         {progress: this.getDeviceStatus(), controller: this}<br><br>
 *
 *     onCancelWriteToDevice is called when the controller is told to cancel writing
 *         to the device {controller: this}<br><br>
 *
 * @requires Prototype
 * @requires BrowserDetect
 * @requires Garmin.DevicePlugin
 * @requires Garmin.Broadcaster
 * @requires Garmin.XmlConverter
 * 
 * @constructor 
 */
Garmin.DeviceControl = function(){}; //just here for jsdoc
Garmin.DeviceControl = Class.create();
Garmin.DeviceControl.prototype = {

    /**
     * Instantiates a Garmin.DeviceControl object

     * @member Garmin.DeviceControl
     */
	initialize: function() {

		try {
			if (typeof(Garmin.DevicePlugin) == 'undefined') throw '';
		} catch(e) {
			throw 'Garmin.DeviceControl depends on the Garmin.DevicePlugin framework.';
		};

		var pluginElement;
		if( window.ActiveXObject ) { // IE
			pluginElement = $("GarminActiveXControl");
		} else { // FireFox
			pluginElement = $("GarminNetscapePlugin");
		}
		
		if (pluginElement == null) throw (new Error("Plug-In HTML tag not found.")).name = "HtmlTagNotFoundException";
		this.garminPlugin = new Garmin.DevicePlugin(pluginElement);
		this._validatePlugin();
		
		this._broadcaster = new Garmin.Broadcaster();

		this.getDetailedDeviceData = true;
		this.devices = new Array();
		this.deviceNumber = null;
		this.numDevices = 0;

		this.gpsData = null;
		this.gpsDataType = null;		
		this.gpsDataString = "";
		
		this.state = BusyState.idle;
	},

    /**
     * Writes an address to the currently selected device.
     * 
     * @param {String} address to be written to the device. This doesn't check validity
     * @member Garmin.DeviceControl
     */	
	writeAddressToDevice: function(address) {
		if (!this.geocoder) {
			this.geocoder = new Garmin.Geocode();
			this.geocoder.register(this);
		}
		this.geocoder.findLatLng(address);
	},

	/**
	 * Handles call-back from geocoder and forwards call to onException on registered listeners.
	 * @private
     * @param {Error} error wrapped in JSON 'msg' object.
     * @member Garmin.DeviceControl
     */
	onException: function(json) {
		this._reportException(json.msg);
	},
	
	/**
	 * Handles call-back from geocoder and forwards call to writeToDevice.
	 * Registered listeners will recieve an onFinishedFindLatLon call before writeToDevice is invoked.
	 * @private
     * @param {Object} waypoint wrapped in JSON 'controller' object.
     * @member Garmin.DeviceControl
     */
	onFinishedFindLatLon: function(json) {
		var fileName = "address.gpx";
		var latLng = json.waypoint;
		this._broadcaster.dispatch("onFinishedFindLatLon", {waypoint: latLng});
   		var factory = new Garmin.GpsDataFactory();
		var gpxStr = factory.produceGpxString(null, [latLng]);
		this.writeToDevice(gpxStr, fileName);
	},
	

	/**
	 * @private
     * @throws BrowserNotSupportedException
     * @throws PluginNotInstalledException
     * @throws OutOfDatePluginException
     * @member Garmin.DeviceControl
     */
    _validatePlugin: function() {
    	// TODO: make sure these exceptions work properly in all browsers, stupid JS
    	if(!this.isBrowserSupported()) {
    	    var notSupported = new Error("Your broswer is not supported to use the Garmin Communicator Plug-In.");
    	    notSupported.name = "BrowserNotSupportedException";
    	    throw notSupported;
        }
		if (!this.isPluginInstalled()) {
    	    var notInstalled = new Error("You need to have the Garmin Communicator Plug-In installed to connect to your device.");
    	    notInstalled.name = "PluginNotInstalledException";
    	    throw notInstalled;
        }
		if(this.isPluginOutOfDate()) {
    	    var outOfDate = new Error("Your version of the Garmin Communicator Plug-In is out of date.  Please upgrade.");
    	    outOfDate.name = "OutOfDatePluginException";
    	    outOfDate.version = this.getPluginVersionString();
    	    throw outOfDate;
        } 
    },

	/**
     * Unlocks the GpsControl object to be used at the given web adress.
     * 
     * @param {String} web_path 
     * @param {String} unlock_code
     * @type Boolean 
     * @return True if the plug-in was unlocked successfully
     * @member Garmin.DeviceControl
     */
	unlock: function(web_path, unlock_code) {
		return this.garminPlugin.unlock(web_path, unlock_code);
	},

	/**
     * Register to be an event listener.  An object that is registered will be dispatched
     * a method if they have a function with the same dispatch name.  So if you register a
     * listener with an onFinishFindDevices, and the onFinishFindDevices message is called, you'll
     * get that message.  See class comments for event types
     *
     * @param {Object} Object that will listen for events coming from this object 
     * @see {Garmin.Broadcaster}
     * @member Garmin.DeviceControl
     */	
	register: function(listener) {
        this._broadcaster.register(listener);
	},

	/**
     * Finds any connected Garmin Devices.  
     * When it's done finding the devices, onFinishFindDevices is dispatched
     * this.numDevices = the number of devices found
     * this.deviceNumber is the device that we'll use to communicate with
     * Use this.getDevices() to get an array of the found devices and 
     * this.setDeviceNumber({Number}) to change the device
     *
     * @member Garmin.DeviceControl
     */	
	findDevices: function() {
        this.garminPlugin.startFindDevices();
	    this._broadcaster.dispatch("onStartFindDevices", {controller: this});
        setTimeout(function() { this._finishFindDevices() }.bind(this), 1000);
	},

	/**
     * Cancels the current find devices interaction
	 *
     * @member Garmin.DeviceControl
     */	
	cancelFindDevices: function() {
		this.garminPlugin.cancelFindDevices();
		this.state = BusyState.idle;
    	this._broadcaster.dispatch("onCancelFindDevices", {controller: this});
	},

	/**
	 * @private
     * @member Garmin.DeviceControl
     */	
	_finishFindDevices: function() {
    	if(this.garminPlugin.finishFindDevices()) {
            var xmlDevicesString = this.garminPlugin.getDevicesXml();
            var xmlDevicesDoc = Garmin.XmlConverter.toDocument(xmlDevicesString); 
            
            var deviceList = xmlDevicesDoc.getElementsByTagName("Device");
            this.devices = new Array();
            this.numDevices = deviceList.length;
            
        	for( var i=0; i < this.numDevices; i++ ) {
				var displayName = deviceList[i].getAttribute("DisplayName");        		
        		var deviceNumber = parseInt( deviceList[i].getAttribute("Number") );        		        		        		
				var deviceDescriptionXml = this.garminPlugin.getDeviceDescriptionXml(deviceNumber);				 		
				var deviceDescriptionDoc = Garmin.XmlConverter.toDocument(deviceDescriptionXml);    

        		this.devices.push(this._createDevice(displayName, deviceNumber, deviceDescriptionDoc));              		  		
        		// just use the last one found for now
        		this.deviceNumber = deviceNumber;
        	}
        	
	        this._broadcaster.dispatch("onFinishFindDevices", {controller: this});
	        
	        switch(this.state) {
	        	case BusyState.idle:
	        		break;
	        	case BusyState.downloading:
	        		this.downlaodToDevice(this.writeXml, this.writeFilename);
	        		break;
	        	case BusyState.writing:
	        		this.writeToDevice(this.writeXml, this.writeFilename);
	        		break;
	        	case BusyState.reading:
	        		this._readDataFromDevice();
	        		break;
	        }	        
    	} else {
    		setTimeout(function() { this._finishFindDevices() }.bind(this), 500);
    	}
	},

	/**
	 * @private
	 * @member Garmin.DeviceControl
	 */
	_createDevice: function(displayName, deviceNumber, deviceDescriptionDoc) {
   		var device = new Garmin.Device(displayName, deviceNumber);

   		if(this.getDetailedDeviceData) {						
			var partNumber = deviceDescriptionDoc.getElementsByTagName("PartNumber")[0].childNodes[0].nodeValue;
			var softwareVersion = deviceDescriptionDoc.getElementsByTagName("SoftwareVersion")[0].childNodes[0].nodeValue;
			var description = deviceDescriptionDoc.getElementsByTagName("Description")[0].childNodes[0].nodeValue;
			var id = deviceDescriptionDoc.getElementsByTagName("Id")[0].childNodes[0].nodeValue;
			
			device.setPartNumber(partNumber);
			device.setSoftwareVersion(softwareVersion);
			device.setDescription(description);
			device.setId(id);
			
			var dataTypeList = deviceDescriptionDoc.getElementsByTagName("DataType");
			var numOfDataTypes = dataTypeList.length;
	
			for ( var j = 0; j < numOfDataTypes; j++ ) {
				var dataName = dataTypeList[j].getElementsByTagName("Name")[0].childNodes[0].nodeValue;					
				var dataExt = dataTypeList[j].getElementsByTagName("FileExtension")[0].childNodes[0].nodeValue;
	
				var dataType = new Garmin.DeviceDataType(dataName, dataExt);
				var fileList = dataTypeList[j].getElementsByTagName("File");
				var numOfFiles = fileList.length;
				
				for ( var k = 0; k < numOfFiles; k++ ) {
					var transferDir = fileList[k].getElementsByTagName("TransferDirection")[0].childNodes[0].nodeValue;											
					if ((transferDir == TransferDirection.read)) {
						dataType.setReadAccess(true);
					} else if ((transferDir == TransferDirection.write)) {			
						dataType.setWriteAccess(true);
					} else if ((transferDir == TransferDirection.both)) {		
						dataType.setReadAccess(true);
						dataType.setWriteAccess(true);
					}		
				}			
				device.addDeviceDataType(dataType);
			}   			
   		}
		return device;
	},

	/**
     * Sets the deviceNumber variable which determines which connected device to talk to.
     * @param {Number} The device number
     * @member Garmin.DeviceControl
     */	
	setDeviceNumber: function(deviceNumber) {
		this.deviceNumber = deviceNumber;
	},

	/**
     * Get a list of the devices found
     * @type Array<Garmin.Device>
     * @member Garmin.DeviceControl
     */	
	getDevices: function() {
		return this.devices;
	},

	/**
     * Asynchronously reads data from the connected device.  Only handles reading
     * from the device in this.deviceNumber
     * 
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString and this.gpsData
     * 
     * @member Garmin.DeviceControl
     */
	readFromDevice: function() {
		this.gpsDataType = GpsFileType.gpx;
		this._readDataFromDevice();
	},
	
	/**
     * Asynchronously reads fotmess data from the connected device.  Only handles 
     * reading from the device in this.deviceNumber
     * 
     * When the data has been gathered, the onFinishedReadFromDevice is fired, and the
     * data is stored in this.gpsDataString
     * 
     * @member Garmin.DeviceControl
     */	
	readFromDeviceFitness: function() {	
		this.gpsDataType = GpsFileType.tcx;
		this._readDataFromDevice();
	},
	
	/**
	 * @private
     * @member Garmin.DeviceControl
     */	
	_readDataFromDevice: function() {
		this.state = BusyState.reading;
		this.gpsData = null;		
		this.gpsDataString = null;
		if (this.numDevices == 0) {
			this.findDevices();			
		} else {
			this.idle = false;
			try {
	        	this._broadcaster.dispatch("onStartReadFromDevice", {controller: this});
	        	if (this.gpsDataType == GpsFileType.gpx) {
			    	this.garminPlugin.startReadFromGps( this.deviceNumber );	        		
	        	} else if (this.gpsDataType == GpsFileType.tcx) {
			    	this.garminPlugin.startReadFitnessData( this.deviceNumber, "HST" );	        		
	        	}
			    this._progressRead();    
			} catch(e) {
			    this._reportException(e);
			}
		}		
	},
	
	/**
	 * @private
     * @member Garmin.DeviceControl
     */	
	_progressRead: function() {
		this._broadcaster.dispatch("onProgressReadFromDevice", {progress: this.getDeviceStatus(), controller: this});
        setTimeout(function() { this._finishReadFromDevice() }.bind(this), 200);			 
	},
	
	/**
	 * @private
     * @member Garmin.DeviceControl
     */	
	_finishReadFromDevice: function() {
		var completionState;	
		if (this.gpsDataType == GpsFileType.gpx) {
		   completionState = this.garminPlugin.finishReadFromGps();				
		} else if (this.gpsDataType == GpsFileType.tcx) {
		   completionState = this.garminPlugin.finishReadFitnessData();		
		} else {
			//todo: throw some error		
		}

		var readFrom = {
	        completionState: completionState,
	        onFinishDispatch: "onFinishReadFromDevice",
	        onWaitingDispatch: "onWaitingReadFromDevice",
	        progressDispatchFunction: this._progressRead.bind(this)
	    };	

		this._finishInteraction(readFrom);
	},
	
	/**
     * Cancels the current read from the device
	 *
     * @member Garmin.DeviceControl
     */	
	cancelReadFromDevice: function() {
		if (this.gpsDataType == GpsFileType.gpx) {
			this.garminPlugin.cancelReadFromGps();
		} else if (this.gpsDataType == GpsFileType.tcx) {
			this.garminPlugin.cancelReadFitnessData();
		}
		this.state = BusyState.idle;
    	this._broadcaster.dispatch("onCancelReadFromDevice", {controller: this});
	},

    /**
     * Writes the given xml to the device selected in this.deviceNumber
     * @param {String} xmlString to be written to the device. This doesn't check validity
     * @param {String} fileName to write it to.  Validity is not checked here
     * @member Garmin.DeviceControl
     */	
	writeToDevice: function(xmlString, fileName) {
		this.state = BusyState.writing;
		this.gpsDataType = GpsFileType.gpx;
		if(this.numDevices == 0) {
			this.writeXml = xmlString;
			this.writeFilename = fileName;
			this.findDevices();
		} else {
			try {
	        	this._broadcaster.dispatch("onStartWriteToDevice", {controller: this});
			    this.garminPlugin.startWriteToGps(xmlString, fileName, this.deviceNumber);
			    this._progressWrite();
		    } catch(e) {
				this._reportException(e);
		   	}
		}
	},

	/**
     * Writes GPI info to the device selected in this.deviceNumber.
     *
     * @param {String} xmlString to be written to the device. This doesn't check validity
     * @param {String} fileName to write it to.  Validity is not checked here
     * @member Garmin.DeviceControl
     */	
	downlaodToDevice: function(gpiDataString, filename) {
		this.state = BusyState.downloading;
		if(this.numDevices == 0) {
			this.writeXml = gpiDataString;
			this.writeFilename = fileName;
			this.findDevices();
		} else {
			try {
			    this.garminPlugin.startDownloadData(gpsDataString, filename, this.deviceNumber );
			    this._progressWrite();
		    } catch(e) {
				this._reportException(e);
		    }
		}
	},
	
	/**
	 * @private
     * @member Garmin.DeviceControl
     */	
	_progressWrite: function() {
    	this._broadcaster.dispatch("onProgressWriteToDevice", {progress: this.getDeviceStatus(), controller: this});
        setTimeout(function() { this._finishWriteToDevice() }.bind(this), 200);
	},
	
	/**
	 * @private
     * @member Garmin.DeviceControl
     */	
	_finishWriteToDevice: function() {
	    var writeTo = {
	        completionState: this.garminPlugin.finishWriteToGps(),
	        onFinishDispatch: "onFinishWriteToDevice",
	        onWaitingDispatch: "onWaitingWriteToDevice",
	        progressDispatchFunction: this._progressWrite.bind(this)
	    };
		this._finishInteraction(writeTo);
	},
    
	/**
     * Cancels the current write transfer to the device
     * @member Garmin.DeviceControl
     */	
	cancelWriteToDevice: function() {
		this.garminPlugin.cancelWriteToGps();
		this.state = BusyState.idle;
		this._broadcaster.dispatch("onCancelWriteToDevice", {controller: this});
	},

    /**
     * Responds to a message box on the device.  
     * @param {Number} response should be an int which corresponds to a button value from this.garminPlugin.MessageBoxXml
     * @member Garmin.DeviceControl
     */
    respondToMessageBox: function(response) {
        this.garminPlugin.respondToMessageBox(response ? 1 : 2);
        this._progressWrite();
    },

    /**
     * @param type determines if we are ReadFrom or WriteTo the device
     * @member Garmin.DeviceControl
     */	
    _finishInteraction: function(type) {
        try {
			if( type.completionState == CompletionState.finished ) {
				this.state = BusyState.idle;
				if(this.gpsDataType == GpsFileType.gpx && this.garminPlugin.gpsTransferSucceeded()) {
					this.gpsDataString = this.garminPlugin.getGpsXml();
					this.gpsData = Garmin.XmlConverter.toDocument(this.gpsDataString);
					this._broadcaster.dispatch(type.onFinishDispatch, {success: this.garminPlugin.gpsTransferSucceeded(), controller: this});					
				} else if (this.gpsDataType == GpsFileType.tcx && this.garminPlugin.fitnessTransferSucceeded()) {
					this.gpsDataString = this.garminPlugin.getTcdXml();
					this.gpsData = Garmin.XmlConverter.toDocument(this.gpsDataString);
					this._broadcaster.dispatch(type.onFinishDispatch, {success: this.garminPlugin.fitnessTransferSucceeded(), controller: this});				
				}
			} else if( type.completionState == CompletionState.messageWaiting ) {
				var messageDoc = Garmin.XmlConverter.toDocument(this.garminPlugin.getMessageBoxXml());
//				var type = messageDoc.getElementsByTagName("Icon")[0].childNodes[0].nodeValue;
				var text = messageDoc.getElementsByTagName("Text")[0].childNodes[0].nodeValue;
				
				var message = new Garmin.MessageBox("Question",text);
				
				var buttonNodes = messageDoc.getElementsByTagName("Button");
				for(var i=0; i<buttonNodes.length; i++) {
					var caption = buttonNodes[i].getAttribute("Caption");
					var value = buttonNodes[i].getAttribute("Value");
					message.addButton(caption, value);
				}

				this._broadcaster.dispatch(type.onWaitingDispatch, {message: message, controller: this});
			} else {
	    	    type.progressDispatchFunction();
			}
		} catch( aException ) {
			this._reportException( aException );
		}
    },
    
	/**
     * Get the status/progress of the current state or transfer
     * @type Garmin.TransferProgress
     * @member Garmin.DeviceControl
     */	
	getDeviceStatus: function() {
		var aProgressXml = this.garminPlugin.getProgressXml();
		var theProgressDoc = Garmin.XmlConverter.toDocument(aProgressXml);
		
		var title = "";
		if(theProgressDoc.getElementsByTagName("Title").length > 0) {
			title = theProgressDoc.getElementsByTagName("Title")[0].childNodes[0].nodeValue;
		}
		
		var progress = new Garmin.TransferProgress(title);

		var textNodes = theProgressDoc.getElementsByTagName("Text");
		for( var i=0; i < textNodes.length; i++ ) {
			if(textNodes[i].childNodes.length > 0) {
				var text = textNodes[i].childNodes[0].nodeValue;
				if(text != "") progress.addText(text);
			}
		}
		
		var percentageNode = theProgressDoc.getElementsByTagName("ProgressBar")[0];
		if(percentageNode != undefined) {
			if(percentageNode.getAttribute("Type") == "Percentage") {
				progress.setPercentage(percentageNode.getAttribute("Value"));
			} else if (percentageNode.getAttribute("Type") == "Indefinite") {
				progress.setPercentage(100);			
			}
		}

		return progress;
	},
		
	/**
     * Gets the version number for the plugin the user has currently installed
     * @type Array 
     * @return An array of the format: [versionMajor, versionMinor, buildMajor, buildMinor].
     * @member Garmin.DeviceControl
     */	
	getPluginVersion: function() {
    	var theVersionDocument = Garmin.XmlConverter.toDocument(this.garminPlugin.getVersionXml());
    	
    	var versionMajor = parseInt(theVersionDocument.getElementsByTagName("VersionMajor")[0].firstChild.nodeValue);
    	var versionMinor = parseInt(theVersionDocument.getElementsByTagName("VersionMinor")[0].firstChild.nodeValue);
    	var buildMajor = parseInt(theVersionDocument.getElementsByTagName("BuildMajor")[0].firstChild.nodeValue);
    	var buildMinor = parseInt(theVersionDocument.getElementsByTagName("BuildMinor")[0].firstChild.nodeValue);

	    var versionArray = [versionMajor, versionMinor, buildMajor, buildMinor];
	    return versionArray;
	},

	/**
	 * Determines if the Garmin agent is the current version.  See RequiredVersion object below to see the
	 * current required version.
     * @type Boolean 
     * @member Garmin.DeviceControl
	 */
	isPluginOutOfDate: function() {
    	var version = this.getPluginVersion();
        return version[0] < RequiredVersion.versionMajor || ((version[0] == RequiredVersion.versionMajor) && version[1] < RequiredVersion.versionMinor);
	},

	/**
     * Gets a string of the version number for the plugin the user has currently installed
     * @type String 
     * @return A string of the format "versionMajor.versionMinor.buildMajor.buildMinor", ex: "2.0.0.4"
     * @member Garmin.DeviceControl
     */	
	getPluginVersionString: function() {
		var versionArray = this.getPluginVersion();
	
		var versionString = versionArray[0] + "." + versionArray[1] + "." + versionArray[2] + "." + versionArray[3];
	    return versionString;
	},

	/**
     * Determines if the plugin is initialized
     * @type Boolean
     * @member Garmin.DeviceControl
     */	
	isPluginInitialized: function() {
		return (this.garminPlugin != null);
	},

	/**
     * Determines if the plugin is installed on the user's machine
     * @type Boolean
     * @member Garmin.DeviceControl
     */	
	isPluginInstalled: function() {
		return (this.garminPlugin.getVersionXml() != undefined);
	},

    /**
     * Determines if the users browser is currently supported by the plugin
     * @type Boolean
     * @member Garmin.DeviceControl
     */	 
	isBrowserSupported: function() {
		return (BrowserDetect.OS == "Windows" && (BrowserDetect.browser == "Firefox" || BrowserDetect.browser == "Explorer"));
	},

	/**
	 * @private
     * @member Garmin.DeviceControl
     */	
	_reportException: function(exception) {
		this._broadcaster.dispatch("onException", {msg: exception});
	},
	
	/**
	 * @type String
     * @member Garmin.DeviceControl
     */	
	toString: function() {
	    return "Garmin Javascript GPS Controller managing " + this.numDevices + " device(s)";
	}
};

/**
 * Current Version of the Garmin Communicator Plugin, and a complementary toString function to print it out with
 */
var RequiredVersion = {
    versionMajor: 2,
    versionMinor: 0,
    buildMajor: 0,
    buildMinor: 0,
    
    toString: function() {
        return this.versionMajor + "." + this.versionMinor + "." + this.buildMajor + "." + this.buildMinor;
    }
};

/**
 * The Object returns a BusyState as enums defined as this for lazy loading of devices 
 */
var BusyState = {
	idle: 0,
	reading: 1,
	writing: 2,
	downloading: 3
};

/**
 * The Object returns a CompletionState as enums defined as this when you poll the finishActions
 */
var CompletionState = {
	idle: 0,
	working: 1,
	messageWaiting: 2,
	finished: 3
};

/**
 * The Object returns a GpsFileType as enums defining possible file types associated with read and write 
 * methods
 */
var GpsFileType = {
	gpx: 0,
	tcx: 1
};

/**
 * The Object returns a TransferDirection as enums defining the strings used by the Device.xml to indicate 
 * transfer direction of each file type
 */
var TransferDirection = {
	read: "OutputFromUnit",
	write: "InputToUnit",
	both: "InputOutput"
};

/**
 * @class Garmin.TransferProgress
 * Encapsulates the data provided by the device for the current process' progress.
 * Use this to relay progress information to the user.
 * @constructor 
 */
Garmin.TransferProgress = Class.create();
Garmin.TransferProgress.prototype = {
	initialize: function(title) {
		this.title = title;
		this.text = new Array();
		this.percentage = null;
	},
	
	addText: function(textString) {
		this.text.push(textString);
	},

    /**
     * Get all the text entries for the transfer
     * @type Array
     * @member Garmin.TransferProgress
     */	 
	getText: function() {
		return this.text;
	},

    /**
     * Get the title for the transfer
     * @type String
     * @member Garmin.TransferProgress
     */	 
	getTitle: function() {
		return this.title;
	},
	
	setPercentage: function(percentage) {
		this.percentage = percentage;
	},

    /**
     * Get the completed percentage value for the transfer
     * @type Number
     * @member Garmin.TransferProgress
     */
	getPercentage: function() {
		return this.percentage;
	},

    /**
     * @type String
     * @member Garmin.TransferProgress
     */	 	
	toString: function() {
		var progressString = "";
		if(this.getTitle() != null) {
			progressString += this.getTitle();
		}
		if(this.getPercentage() != null) {
			progressString += ": " + this.getPercentage() + "%";
		}
		return progressString;
	}
};


/**
 * @class Garmin.MessageBox
 * Encapsulates the data to display a message box to the user when the plug-in is waiting for feedback
 * @constructor 
 */
Garmin.MessageBox = Class.create();
Garmin.MessageBox.prototype = {
	initialize: function(type, text) {
		this.type = type;
		this.text = text;
		this.buttons = new Array();
	},

    /**
     * Get the type of the message box
     * @type String
     * @member Garmin.MessageBox
     */	 
	getType: function() {
		return this.type;
	},

    /**
     * Get the text entry for the message box
     * @type String
     * @member Garmin.MessageBox
     */	 
	getText: function() {
		return this.text;
	},

    /**
     * Get the text entry for the message box
     * @member Garmin.MessageBox
     */	 
	addButton: function(caption, value) {
		this.buttons.push({caption: caption, value: value});
	},

    /**
     * Get the buttons for the message box
     * @type Array
     * @member Garmin.MessageBox
     */	 
	getButtons: function() {
		return this.buttons;
	},
	
	getButtonValue: function(caption) {
		for(var i=0; i< this.buttons.length; i++) {
			if(this.buttons[i].caption == caption) {
				return this.buttons[i].value;
			}
		}
		return null;
	},

    /**
	 * @type String
     * @member Garmin.MessageBox
     */	 
	toString: function() {
		return this.getText();
	}
};

// Dynamic include of required libraries and check for Prototype
// Code taken from scriptaculous
// TODO: put this code in a library and reuse is instead of copying it to new files
var Control = {
	require: function(libraryName) {
	  // inserting via DOM fails in Safari 2.0, so brute force approach
	  document.write('<script type="text/javascript" src="'+libraryName+'"></script>');
	},

	load: function() {
	  if((typeof Prototype=='undefined') || 
	     (typeof Element == 'undefined') || 
	     (typeof Element.Methods=='undefined') ||
	     parseFloat(Prototype.Version.split(".")[0] + "." +
	                Prototype.Version.split(".")[1]) < 1.5)
	     throw("Garmin.DeviceControl requires the Prototype JavaScript framework >= 1.5.0");
	  
	  $A(document.getElementsByTagName("script")).findAll( function(s) {
	    return (s.src && s.src.match(/GarminDeviceControl\.js(\?.*)?$/))
	  }).each( function(s) {
	    var path = s.src.replace(/GarminDeviceControl\.js(\?.*)?$/,'');
	    var includes = s.src.match(/\?.*load=([a-z,]*)/);
	    (includes ? includes[1] : 'GarminDevicePlugin,GarminGpsDataStructures,GoogleMapController,GarminDevice,Util-XmlConverter,Util-Broadcaster,Util-DateTimeFormat,Util-BrowserDetect').split(',').each(
	     function(include) { Control.require(path+include+'.js') });
	  });
	}
}

Control.load();