dojo.provide("dojox.off.files");

// Author: Brad Neuberg, bkn3@columbia.edu, http://codinginparadise.org

// summary:
//	Helps maintain resources that should be
//	available offline, such as CSS files.
// description:
//	dojox.off.files makes it easy to indicate
//	what resources should be available offline,
//	such as CSS files, JavaScript, HTML, etc.
dojox.off.files = {
	// versionURL: String
	//	An optional file, that if present, records the version
	//	of our bundle of files to make available offline. If this
	//	file is present, and we are not currently debugging,
	//	then we only refresh our offline files if the version has
	//	changed. 
	versionURL: "version.js",
	
	// listOfURLs: Array
	//	For advanced usage; most developers can ignore this.
	//	Our list of URLs that will be cached and made available
	//	offline.
	listOfURLs: [],
	
	// refreshing: boolean
	//	For advanced usage; most developers can ignore this.
	//	Whether we are currently in the middle
	//	of refreshing our list of offline files.
	refreshing: false,

	_cancelID: null,
	
	_error: false,
	_errorMessages: [],
	_currentFileIndex: 0,
	_store: null,
	_doSlurp: false,
	
	slurp: function(){
		// summary:
		//	Autoscans the page to find all resources to
		//	cache. This includes scripts, images, CSS, and hyperlinks
		//	to pages that are in the same scheme/port/host as this
		//	page. We also scan the embedded CSS of any stylesheets
		//	to find @import statements and url()'s.
		//  You should call this method from the top-level, outside of
		//	any functions and before the page loads:
		//
		//	<script>
		//		dojo.require("dojox.sql");
		//		dojo.require("dojox.off");
		//		dojo.require("dojox.off.ui");
		//		dojo.require("dojox.off.sync");
		//
		//		// configure how we should work offline
		//
		//		// set our application name
		//		dojox.off.ui.appName = "Moxie";
		//
		//		// automatically "slurp" the page and
		//		// capture the resources we need offline
		//		dojox.off.files.slurp();
		//
		// 		// tell Dojo Offline we are ready for it to initialize itself now
		//		// that we have finished configuring it for our application
		//		dojox.off.initialize();
		//	</script>
		//
		//	Note that inline styles on elements are not handled (i.e.
		//	if you somehow have an inline style that uses a URL);
		//	object and embed tags are not scanned since their format
		//	differs based on type; and elements created by JavaScript
		//	after page load are not found. For these you must manually
		//	add them with a dojox.off.files.cache() method call.
		
		// just schedule the slurp once the page is loaded and
		// Dojo Offline is ready to slurp; dojox.off will call
		// our _slurp() method before indicating it is finished
		// loading
		this._doSlurp = true;
	},
	
	cache: function(urlOrList){ /* void */
		// summary:
		//		Caches a file or list of files to be available offline. This
		//		can either be a full URL, such as http://foobar.com/index.html,
		//		or a relative URL, such as ../index.html. This URL is not
		//		actually cached until dojox.off.sync.synchronize() is called.
		// urlOrList: String or Array[]
		//		A URL of a file to cache or an Array of Strings of files to
		//		cache
		
		//console.debug("dojox.off.files.cache, urlOrList="+urlOrList);
		
		if(dojo.isString(urlOrList)){
			var url = this._trimAnchor(urlOrList+"");
			if(!this.isAvailable(url)){ 
				this.listOfURLs.push(url); 
			}
		}else if(urlOrList instanceof dojo._Url){
			var url = this._trimAnchor(urlOrList.uri);
			if(!this.isAvailable(url)){ 
				this.listOfURLs.push(url); 
			}
		}else{
			dojo.forEach(urlOrList, function(url){
				url = this._trimAnchor(url);
				if(!this.isAvailable(url)){ 
					this.listOfURLs.push(url); 
				}
			}, this);
		}
	},
	
	printURLs: function(){
		// summary:
		//	A helper function that will dump and print out
		//	all of the URLs that are cached for offline
		//	availability. This can help with debugging if you
		//	are trying to make sure that all of your URLs are
		//	available offline
		console.debug("The following URLs are cached for offline use:");
		dojo.forEach(this.listOfURLs, function(i){
			console.debug(i);
		});	
	},
	
	remove: function(url){ /* void */
		// summary:
		//		Removes a URL from the list of files to cache.
		// description:
		//		Removes a URL from the list of URLs to cache. Note that this
		//		does not actually remove the file from the offline cache;
		//		instead, it just prevents us from refreshing this file at a
		//		later time, so that it will naturally time out and be removed
		//		from the offline cache
		// url: String
		//		The URL to remove
		for(var i = 0; i < this.listOfURLs.length; i++){
			if(this.listOfURLs[i] == url){
				this.listOfURLs = this.listOfURLs.splice(i, 1);
				break;
			}
		}
	},
	
	isAvailable: function(url){ /* boolean */
		// summary:
		//		Determines whether the given resource is available offline.
		// url: String
		//	The URL to check
		for(var i = 0; i < this.listOfURLs.length; i++){
			if(this.listOfURLs[i] == url){
				return true;
			}
		}
		
		return false;
	},
	
	refresh: function(callback){ /* void */
		//console.debug("dojox.off.files.refresh");
		// summary:
		//	For advanced usage; most developers can ignore this.
		//	Refreshes our list of offline resources,
		//	making them available offline.
		// callback: Function
		//	A callback that receives two arguments: whether an error
		//	occurred, which is a boolean; and an array of error message strings
		//	with details on errors encountered. If no error occured then message is
		//	empty array with length 0.
		try{
			if(dojo.config.isDebug){
				this.printURLs();
			}
			
			this.refreshing = true;
			
			if(this.versionURL){
				this._getVersionInfo(function(oldVersion, newVersion, justDebugged){
					//console.warn("getVersionInfo, oldVersion="+oldVersion+", newVersion="+newVersion
					//				+ ", justDebugged="+justDebugged+", isDebug="+dojo.config.isDebug);
					if(dojo.config.isDebug || !newVersion || justDebugged 
							|| !oldVersion || oldVersion != newVersion){
						console.warn("Refreshing offline file list");
						this._doRefresh(callback, newVersion);
					}else{
						console.warn("No need to refresh offline file list");
						callback(false, []);
					}
				});
			}else{
				console.warn("Refreshing offline file list");
				this._doRefresh(callback);
			}
		}catch(e){
			this.refreshing = false;
                       
			// can't refresh files -- core operation --
			// fail fast
			dojox.off.coreOpFailed = true;
			dojox.off.enabled = false;
			dojox.off.onFrameworkEvent("coreOperationFailed");
		}
	},
	
	abortRefresh: function(){
		// summary:
		//	For advanced usage; most developers can ignore this.
		//	Aborts and cancels a refresh.
		if(!this.refreshing){
			return;
		}
		
		this._store.abortCapture(this._cancelID);
		this.refreshing = false;
	},
	
	_slurp: function(){
		if(!this._doSlurp){
			return;
		}
		
		var handleUrl = dojo.hitch(this, function(url){
			if(this._sameLocation(url)){
				this.cache(url);
			}
		});
		
		handleUrl(window.location.href);
		
		dojo.query("script").forEach(function(i){
			try{
				handleUrl(i.getAttribute("src"));
			}catch(exp){
				//console.debug("dojox.off.files.slurp 'script' error: " 
				//				+ exp.message||exp);
			}
		});
		
		dojo.query("link").forEach(function(i){
			try{
				if(!i.getAttribute("rel")
					|| i.getAttribute("rel").toLowerCase() != "stylesheet"){
					return;
				}
			
				handleUrl(i.getAttribute("href"));
			}catch(exp){
				//console.debug("dojox.off.files.slurp 'link' error: " 
				//				+ exp.message||exp);
			}
		});
		
		dojo.query("img").forEach(function(i){
			try{
				handleUrl(i.getAttribute("src"));
			}catch(exp){
				//console.debug("dojox.off.files.slurp 'img' error: " 
				//				+ exp.message||exp);
			}
		});
		
		dojo.query("a").forEach(function(i){
			try{
				handleUrl(i.getAttribute("href"));
			}catch(exp){
				//console.debug("dojox.off.files.slurp 'a' error: " 
				//				+ exp.message||exp);
			}
		});
		
		// FIXME: handle 'object' and 'embed' tag
		
		// parse our style sheets for inline URLs and imports
		dojo.forEach(document.styleSheets, function(sheet){
			try{
				if(sheet.cssRules){ // Firefox
					dojo.forEach(sheet.cssRules, function(rule){
						var text = rule.cssText;
						if(text){
							var matches = text.match(/url\(\s*([^\) ]*)\s*\)/i);
							if(!matches){
								return;
							}
							
							for(var i = 1; i < matches.length; i++){
								handleUrl(matches[i])
							}
						}
					});
				}else if(sheet.cssText){ // IE
					var matches;
					var text = sheet.cssText.toString();
					// unfortunately, using RegExp.exec seems to be flakey
					// for looping across multiple lines on IE using the
					// global flag, so we have to simulate it
					var lines = text.split(/\f|\r|\n/);
					for(var i = 0; i < lines.length; i++){
						matches = lines[i].match(/url\(\s*([^\) ]*)\s*\)/i);
						if(matches && matches.length){
							handleUrl(matches[1]);
						}
					}
				}
			}catch(exp){
				//console.debug("dojox.off.files.slurp stylesheet parse error: " 
				//				+ exp.message||exp);
			}
		});
		
		//this.printURLs();
	},
	
	_sameLocation: function(url){
		if(!url){ return false; }
		
		// filter out anchors
		if(url.length && url.charAt(0) == "#"){
			return false;
		}
		
		// FIXME: dojo._Url should be made public;
		// it's functionality is very useful for
		// parsing URLs correctly, which is hard to
		// do right
		url = new dojo._Url(url);
		
		// totally relative -- ../../someFile.html
		if(!url.scheme && !url.port && !url.host){ 
			return true;
		}
		
		// scheme relative with port specified -- brad.com:8080
		if(!url.scheme && url.host && url.port
				&& window.location.hostname == url.host
				&& window.location.port == url.port){
			return true;
		}
		
		// scheme relative with no-port specified -- brad.com
		if(!url.scheme && url.host && !url.port
			&& window.location.hostname == url.host
			&& window.location.port == 80){
			return true;
		}
		
		// else we have everything
		return  window.location.protocol == (url.scheme + ":")
				&& window.location.hostname == url.host
				&& (window.location.port == url.port || !window.location.port && !url.port);
	},
	
	_trimAnchor: function(url){
		return url.replace(/\#.*$/, "");
	},
	
	_doRefresh: function(callback, newVersion){
		// get our local server
		var localServer;
		try{
			localServer = google.gears.factory.create("beta.localserver", "1.0");
		}catch(exp){
			dojo.setObject("google.gears.denied", true);
			dojox.off.onFrameworkEvent("coreOperationFailed");
			throw "Google Gears must be allowed to run";
		}
		
		var storeName = "dot_store_" 
							+ window.location.href.replace(/[^0-9A-Za-z_]/g, "_");
							
		// clip at 64 characters, the max length of a resource store name
		if(storeName.length >= 64){
		  storeName = storeName.substring(0, 63);
		}
			
		// refresh everything by simply removing
		// any older stores
		localServer.removeStore(storeName);
		
		// open/create the resource store
		localServer.openStore(storeName);
		var store = localServer.createStore(storeName);
		this._store = store;

		// add our list of files to capture
		var self = this;
		this._currentFileIndex = 0;
		this._cancelID = store.capture(this.listOfURLs, function(url, success, captureId){
			//console.debug("store.capture, url="+url+", success="+success);
			if(!success && self.refreshing){
				self._cancelID = null;
				self.refreshing = false;
				var errorMsgs = [];
				errorMsgs.push("Unable to capture: " + url);
				callback(true, errorMsgs);
				return;
			}else if(success){
				self._currentFileIndex++;
			}
			
			if(success && self._currentFileIndex >= self.listOfURLs.length){
				self._cancelID = null;
				self.refreshing = false;
				if(newVersion){
					dojox.storage.put("oldVersion", newVersion, null,
									dojox.off.STORAGE_NAMESPACE);
				}
				dojox.storage.put("justDebugged", dojo.config.isDebug, null,
									dojox.off.STORAGE_NAMESPACE);
				callback(false, []);
			}
		});
	},
	
	_getVersionInfo: function(callback){
		var justDebugged = dojox.storage.get("justDebugged", 
									dojox.off.STORAGE_NAMESPACE);
		var oldVersion = dojox.storage.get("oldVersion",
									dojox.off.STORAGE_NAMESPACE);
		var newVersion = null;
		
		callback = dojo.hitch(this, callback);
		
		dojo.xhrGet({
				url: this.versionURL + "?browserbust=" + new Date().getTime(),
				timeout: 5 * 1000,
				handleAs: "javascript",
				error: function(err){
					//console.warn("dojox.off.files._getVersionInfo, err=",err);
					dojox.storage.remove("oldVersion", dojox.off.STORAGE_NAMESPACE);
					dojox.storage.remove("justDebugged", dojox.off.STORAGE_NAMESPACE);
					callback(oldVersion, newVersion, justDebugged);
				},
				load: function(data){
					//console.warn("dojox.off.files._getVersionInfo, load=",data);
					
					// some servers incorrectly return 404's
					// as a real page
					if(data){
						newVersion = data;
					}
					
					callback(oldVersion, newVersion, justDebugged);
				}
		});
	}
}
