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.  
 * 
 * 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.
 * 
 * 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.
 * 
 * Events:
 *     onStartFindDevices called when starting to search for devices.
 *       the object returned is {controller: this} 
 *
 *     onCancelFindDevices is called when the controller is told to cancel finding
 *         devices
 *         {controller: this}
 *
 *     onFinishFindDevices called when the devices are found.
 *       the object returned is {controller: this} 
 *
 *     onException is called when an exception occurs in a method
 *         object passed back is {msg: exception}
 *
 *	   onInteractionWithNoDevice is called when the device is lazy loaded, but finds no devices,
 * 			yet still attempts a read/write action
 * 		{controller: this}
 * 
 *     onStartReadFromDevice is called when the controller is about to start
 *         reading from the device.
 *        {controller: this}
 * 
 *     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} 
 *
 *     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} 
 *
 *     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}
 *
 *     onCancelReadFromDevice is called when the controller is told to cancel reading
 *         from the device
 *         {controller: this}
 *
 *     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} 
 *
 *     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} 
 *
 *     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}
 *
 *     onCancelWriteToDevice is called when the controller is told to cancel writing
 *         to the device
 *         {controller: this}
 * @constructor 
 */
Garmin.DeviceControl = function(){}; //just here for jsdoc
Garmin.DeviceControl = Class.create();
Garmin.DeviceControl.prototype = {

    /**
     * Instantiates a Garmin.DeviceControl object
     * @constructor 
     * @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.gpsDataString = "";

		this.state = BusyState.idle;
	},

	/**
     * 
     * @throws BrowserNotSupportedException
     * @throws PluginNotInstalledException
     * @throws OutOfDatePluginException
     */
    _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
     * @return {Boolean}
     * @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 
     * @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});
	},

	_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 deviceNumber = parseInt( deviceList[i].getAttribute("Number") );
        		var displayName = deviceList[i].getAttribute("DisplayName");
        		
        		var device = new Garmin.Device(displayName, deviceNumber);
        		
        		if(this.getDetailedDeviceData) {
	        		var deviceDescriptionXml = this.garminPlugin.getDeviceDescriptionXml(deviceNumber);
	        		var deviceDescriptionDoc = Garmin.XmlConverter.toDocument(deviceDescriptionXml); 
					
					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);
				}
        		
        		this.devices.push(device);
        		
        		// 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.readFromDevice();
	        		break;
	        }	        
    	} else {
    		setTimeout(function() { this._finishFindDevices() }.bind(this), 500);
    	}
	},


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

	/**
     * Get a list of the devices found
     * @return {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.state = BusyState.reading;
		if(this.numDevices == 0) {
			this.findDevices();
		} else {
			this.idle = false;
			try {
	        	this._broadcaster.dispatch("onStartReadFromDevice", {controller: this});
			    this.garminPlugin.startReadFromGps( this.deviceNumber );
			    this._progressRead();
			} catch(e) {
			    this._reportException(e);
			}
		}
	},
	
	_progressRead: function() {
    	this._broadcaster.dispatch("onProgressReadFromDevice", {progress: this.getDeviceStatus(), controller: this});
        setTimeout(function() { this._finishReadFromDevice() }.bind(this), 200);
	},
	
	_finishReadFromDevice: function() {
		var readFrom = {
	        completionState: this.garminPlugin.finishReadFromGps(),
	        onFinishDispatch: "onFinishReadFromDevice",
	        onWaitingDispatch: "onWaitingReadFromDevice",
	        progressDispatchFunction: this._progressRead.bind(this)
	    };
		this._finishInteraction(readFrom);
	},
	
	/**
     * Cancels the current read from the device
	 *
     * @member Garmin.DeviceControl
     */	
	cancelReadFromDevice: function() {
		this.garminPlugin.cancelReadFromGps();
		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;
		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);
		    }
		}
	},
	

	_progressWrite: function() {
    	this._broadcaster.dispatch("onProgressWriteToDevice", {progress: this.getDeviceStatus(), controller: this});
        setTimeout(function() { this._finishWriteToDevice() }.bind(this), 200);
	},
	
	_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
     */
    respondToMessageBox: function(response) {
        this.garminPlugin.respondToMessageBox(response ? 1 : 2);
        this._progressWrite();
    },

    /**
     * @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.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( 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
     * @return {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
     * @return {Array} 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 make updates
     * @return {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
     * @return {String} 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
     * @return {Boolean}
     * @member Garmin.DeviceControl
     */	
	isPluginInitialized: function() {
		return (this.garminPlugin != null);
	},

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

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

	_reportException: function(exception) {
		this._broadcaster.dispatch("onException", {msg: exception});
	},
	
	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
};


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

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

    /**
     * Get the completed percentage value for the transfer
     * @return {Number}
     * @member Garmin.TransferProgress
     */	 	
	getPercentage: function() {
		return this.percentage;
	},
	
	toString: function() {
		var progressString = "";
		progressString += this.getTitle();
		if(this.getPercentage() != null) {
			progressString += ": " + this.getPercentage() + "%";
		}
		return progressString;
	}
};


/**
 * @class Garmin.MessageBox
 * Encapsulates the data provided by the device for the 
 */
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
     * @return {String}
     * @member Garmin.MessageBox
     */	 
	getType: function() {
		return this.type;
	},

    /**
     * Get the text entry for the message box
     * @return {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
     * @return {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;
	},
	
	toString: function() {
		return this.getText();
	}
};

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,XmlConverter,Broadcaster,DateTimeFormat,BrowserDetect').split(',').each(
	     function(include) { Control.require(path+include+'.js') });
	  });
	}
}

Control.load();