if (Garmin == undefined) var Garmin = {};
/** Copyright © 2007 Garmin Ltd. or its subsidiaries.
 *
 * Licensed under the Apache License, Version 2.0 (the 'License')
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *    http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an 'AS IS' BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * 
 * @fileoverview Garmin.DeviceControl A high-level JavaScript API which supports listener and callback functionality.
 * 
 * @author Carlo Latasa carlo.latasa@garmin.com
 * @version 1.0
 */
/** A controller object that can retrieve and send data to a Garmin 
 * device.<br><br>
 * @class Garmin.DeviceControl
 * 
 * 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 = {


	/////////////////////// Plugin Handling Methods ///////////////////////	

    /** Instantiates a Garmin.DeviceControl object, but does not unlock/activate plugin.
     */
	initialize: function() {
		this.pluginActivated = false;
		
		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) {
			var error = new Error("Plug-In HTML tag not found.");
			error.name = "HtmlTagNotFoundException";
			throw error;			
		}
		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 = "";
	},

	/** Checks plugin validity: browser support, installation and version.
	 * @private
     * @throws BrowserNotSupportedException
     * @throws PluginNotInstalledException
     * @throws OutOfDatePluginException
     */
    _validatePlugin: function() {
    	// TODO: make sure these exceptions work properly in all browsers, stupid JS
    	var isSupported = BrowserSupport.isBrowserSupported();
     	if(!isSupported) {
    	    var notSupported = new Error("Your broswer is not supported to use the Garmin Communicator Plug-In.");
    	    notSupported.name = "BrowserNotSupportedException";
    	    throw notSupported;
        }
   		var isInstalled = this.isPluginInstalled();
		if (!isInstalled) {
     	    var notInstalled = new Error("Garmin Communicator Plugin NOT detected.");
    	    notInstalled.name = "PluginNotInstalledException";
    	    throw notInstalled;
        }
		if(this.isPluginOutOfDate()) {
    	    var outOfDate = new Error("Your version of the Garmin Communicator Plug-In is out of date, required: "+RequiredVersion.toString()+", current: "+this.getPluginVersionString());
    	    outOfDate.name = "OutOfDatePluginException";
    	    outOfDate.version = this.getPluginVersionString();
    	    throw outOfDate;
        }
    },

	/** Unlocks the GpsControl object to be used at the given web adress.
     * 
     * @param {Array} pathKeyPairsArray baseURL and key pairs.  
     * @type Boolean 
     * @return True if the plug-in was unlocked successfully
     */
	unlock: function(pathKeyPairsArray) {
		var unlocked = this.garminPlugin.unlock(pathKeyPairsArray);
		this.pluginActivated = unlocked;
		return unlocked;
	},

	/** 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}
     */	
	register: function(listener) {
        this._broadcaster.register(listener);
	},

	/** True if plugin has been successfully created and unlocked.
	 * @type Boolean
	 */
	 isActivated: function() {
	 	return this.pluginActivated;
	 },
	 
	 
	/////////////////////// Device Handling Methods ///////////////////////	

	/** 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
     */	
	findDevices: function() {
		if (!this.isActivated())
			throw new Error("Garmin Plugin not activated");
        this.garminPlugin.startFindDevices();
	    this._broadcaster.dispatch("onStartFindDevices", {controller: this});
        setTimeout(function() { this._finishFindDevices() }.bind(this), 1000);
	},

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

	/** Loads device data into devices array.
	 * @private
     */	
	_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});

    	} else {
    		setTimeout(function() { this._finishFindDevices() }.bind(this), 500);
    	}
	},

	/** Create a Garmin.Device instance for each connected device found.
	 * NOTE: this should be moved into DataStructures.
	 * @private
	 */
	_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
     */	
	setDeviceNumber: function(deviceNumber) {
		this.deviceNumber = deviceNumber;
	},

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


	/////////////////////// Web Drop Methods ///////////////////////
	
    /** Writes an address to the currently selected device.
     * 
     * @param {String} address to be written to the device. This doesn't check validity
     */	
	writeAddressToDevice: function(address) {
		if (!this.isActivated())
			throw new Error("Garmin Plugin not activated");
		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.
     */
	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.
     */
	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]);
		//alert("onFinishedFindLatLon")
		this.writeToDevice(gpxStr, fileName);
	},


	/////////////////////// Read Methods ///////////////////////
	
	/** 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
     */
	readFromDevice: function() {
		if (!this.isActivated())
			throw new Error("Garmin Plugin not activated");
		this.gpsDataType = DeviceFileType.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
     */	
	readFromDeviceFitness: function() {	
		if (!this.isActivated())
			throw new Error("Garmin Plugin not activated");
		this.gpsDataType = DeviceFileType.tcx;
		this._readDataFromDevice();
	},
	
	/** Common internal read handling.
	 * @private
     */	
	_readDataFromDevice: function() {
		//this.state = BusyState.reading;
		this.gpsData = null;		
		this.gpsDataString = null;
		if (this.numDevices == 0) {
			throw new Error("No device connected, can't read data.");
		} else {
			this.idle = false;
			try {
	        	this._broadcaster.dispatch("onStartReadFromDevice", {controller: this});
	        	if (this.gpsDataType == DeviceFileType.gpx) {
			    	this.garminPlugin.startReadFromGps( this.deviceNumber );	        		
	        	} else if (this.gpsDataType == DeviceFileType.tcx) {
			    	this.garminPlugin.startReadFitnessData( this.deviceNumber, "HST" );	        		
	        	}
			    this._progressRead();    
			} catch(e) {
			    this._reportException(e);
			}
		}		
	},
	
	/** Internal read dispatching and polling delay.
	 * @private
     */	
	_progressRead: function() {
		this._broadcaster.dispatch("onProgressReadFromDevice", {progress: this.getDeviceStatus(), controller: this});
        setTimeout(function() { this._finishReadFromDevice() }.bind(this), 200);        		 
	},
	
	/** Internal read state logic.
	 * @private
     */	
	_finishReadFromDevice: function() {
		var completionState;	
		if (this.gpsDataType == DeviceFileType.gpx) {
		   completionState = this.garminPlugin.finishReadFromGps();				
		} else if (this.gpsDataType == DeviceFileType.tcx) {
		   completionState = this.garminPlugin.finishReadFitnessData();		
		} else {
			var error = new Error("Cannot handle the device file type for reading");
			error.name = "InvalidTypeException";
			this._reportException(error);
		}

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

		this._finishInteraction(readFrom);
	},
	
	/** Cancels the urrent read from the device
     */	
	cancelReadFromDevice: function() {
		if (this.gpsDataType == DeviceFileType.gpx) {
			this.garminPlugin.cancelReadFromGps();
		} else if (this.gpsDataType == DeviceFileType.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
     */	
	writeToDevice: function(xmlString, fileName) {
		if (!this.isActivated())
			throw new Error("Garmin Plugin not activated");
		this.gpsDataType = DeviceFileType.gpx;
		if(this.numDevices == 0) {
			throw new Error("No device connected, can't write data.");
		} 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
     */	
	downlaodToDevice: function(gpiDataString, filename) {
		if (!this.isActivated())
			throw new Error("Garmin Plugin not activated");
		if(this.numDevices == 0) {
			throw new Error("No device connected, can't read data.");
		} else {
			try {
			    this.garminPlugin.startDownloadData(gpsDataString, filename, this.deviceNumber );
			    this._progressWrite();
		    } catch(e) {
				this._reportException(e);
		    }
		}
	},
	
	/** Internal dispatch and polling delay.
	 * @private
     */	
	_progressWrite: function() {
    	this._broadcaster.dispatch("onProgressWriteToDevice", {progress: this.getDeviceStatus(), controller: this});
        setTimeout(function() { this._finishWriteToDevice() }.bind(this), 200);
	},
	
	/** Internal write lifecycle handling.
	 * @private
     */	
	_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
     */	
	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
     */
    respondToMessageBox: function(response) {
        this.garminPlugin.respondToMessageBox(response ? 1 : 2);
        this._progressWrite();
    },

    /** Generic internal lifecycle handling.
     * @param type determines if we are ReadFrom or WriteTo the device
     */	
    _finishInteraction: function(type) {
        try {
			if( type.completionState == CompletionState.finished ) {
				//this.state = BusyState.idle;
				if(this.gpsDataType == DeviceFileType.gpx) {
					if (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 == DeviceFileType.tcx) {
					if (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 {
					var error = new Error("Cannot process the device file type: " + this.gpsDataType);
					error.name = "InvalidTypeException";
					throw error;
				}
			} 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
     */	
	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].
     */	
	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 
	 */
	isPluginOutOfDate: function() {
    	var pVersion = this._majorVersionToNumber(this.getPluginVersion());
   		var rVersion = this._majorVersionToNumber(RequiredVersion.toArray());
        return (pVersion < rVersion);
    	//var version = this.getPluginVersion();
        //return version[0] < RequiredVersion.versionMajor || ((version[0] == RequiredVersion.versionMajor) && version[1] < RequiredVersion.versionMinor);
	},
	
	_majorVersionToNumber: function(versionArray) {
		if (versionArray[1] > 99 || versionArray[2] > 99)
			throw new Error("version segment is greater than 99: "+versionArray);
		return 1000000*versionArray[0] + 10000*versionArray[1] + 100*versionArray[2];
	},

	_versionToNumber: function(versionArray) {
		if (versionArray[1] > 99 || versionArray[2] > 99 || versionArray[3] > 99)
			throw new Error("version segment is greater than 99: "+versionArray);
		return 1000000*versionArray[0] + 10000*versionArray[1] + 100*versionArray[2] + versionArray[3];
	},

	/** 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"
     */	
	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
     */	
	isPluginInitialized: function() {
		return (this.garminPlugin != null);
	},

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

	/** Internal exception handling for asynchronous calls.
	 * @private
      */	
	_reportException: function(exception) {
		this._broadcaster.dispatch("onException", {msg: exception});
	},
	
	/** String representation of instance.
	 * @type String
     */	
	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: 1,
    buildMajor: 0,
    buildMinor: 1,
    
    toString: function() {
        return this.versionMajor + "." + this.versionMinor + "." + this.buildMajor + "." + this.buildMinor;
    },
    
    toArray: function() {
        return [this.versionMajor, this.versionMinor, this.buildMajor, this.buildMinor];
    }
};

/** Dedicated browser support singleton.
 */
var BrowserSupport = {
    /** Determines if the users browser is currently supported by the plugin
     * @type Boolean
     */	 
	isBrowserSupported: function() {
		return (BrowserDetect.OS == "Windows" && (BrowserDetect.browser == "Firefox" || BrowserDetect.browser == "Explorer"));
	}
};


/** 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 DeviceFileType as enums defining possible file types associated with read and write 
 * methods
 */
var DeviceFileType = {
	gpx:	"gpx",
	tcx:	"tcx"
};

/** 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"
};

/** Encapsulates the data provided by the device for the current process' progress.
 * Use this to relay progress information to the user.
 * @class Garmin.TransferProgress
 * @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
     */	 
	getText: function() {
		return this.text;
	},

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

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

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


/** Encapsulates the data to display a message box to the user when the plug-in is waiting for feedback
 * @class Garmin.MessageBox
 * @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
     */	 
	getType: function() {
		return this.type;
	},

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

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

    /** Get the buttons for the message box
     * @type Array
     */	 
	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
     */	 
	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] : 'garmin/device/GarminDevicePlugin,garmin/device/GarminGpsDataStructures,garmin/device/GoogleMapController,garmin/device/GarminDevice,garmin/util/Util-XmlConverter,garmin/util/Util-Broadcaster,garmin/util/Util-DateTimeFormat,garmin/util/Util-BrowserDetect').split(',').each(
	     function(include) { Control.require(path+include+'.js') });
	  });
	}
}

Control.load();
