/* Copyright 2009 Palm, Inc.  All rights reserved. */

var YoutubeAPI = Class.create ({
	
	initialize: function() {
		// TODO read developer secret from somewhere
		this.registrationFailed = false;
	},
	
	initializeOnConnected: function() {
		// try to read device auth data if we have registered before
		this.deviceKeyNumTries = 0;
		var cookie = new Mojo.Model.Cookie("youtube-dev");
		var storedCookie = cookie.get();
		if (storedCookie) {
			Mojo.Log.info("Youtube: API initializeOnConnected: cookie already exists");
			
			this.candidateToken = {};
			this.candidateToken.deviceId = storedCookie.reg1;
			this.candidateToken.deviceKey = this.deobfuscateKey(storedCookie.reg2);
			this.testDeviceKeyIsValid({deviceId: this.candidateToken.deviceId, deviceKey: this.candidateToken.deviceKey});
			
			Mojo.Log.info("Youtube: api initializeOnConnected, deviceKey from cookie is: *%s*", Base64.encode(this.authToken.deviceKey));
		}
		else {
			// get device key and secret from youtube
			this.getDeviceAuth();
		}
	},
	
	// register this device with youtube and save auth info
	getDeviceAuth: function() {
		// authenticate device but
		// first get device's serial number
		if (PalmSystem.version.indexOf("desktop") >= 0) {
			// query youtube for device tokens
			AppAssistant.net.postFormUrlEncoded(this.registerDeviceURL, this.authToken, 
				this.getDeviceAuthCb.bind(this), this.getDeviceAuthErrCb.bind(this));
		}
		else {
			this.deviceInfoReq = new Mojo.Service.Request("palm://com.palm.preferences/systemProperties", {
				method: 'Get',
				parameters: {key : "com.palm.properties.PalmSN"},
				onSuccess: 
					function(response) {
						if (response && response["com.palm.properties.PalmSN"]) {
							this.authToken.deviceSerialNum = response["com.palm.properties.PalmSN"];
						}
						
						// query youtube for device tokens
						AppAssistant.net.postFormUrlEncoded(this.registerDeviceURL, this.authToken, 
							this.getDeviceAuthCb.bind(this), this.getDeviceAuthErrCb.bind(this));
							
						this.deviceInfoReq.cancel();
					}.bind(this),
			});
		}
	}, 
	
	// decrypt and save device key that we got from youtube
	getDeviceAuthCb: function(t) {
		if (t && t.responseText) {
			Mojo.Log.info("Youtube: api getDeviceAuthCb, response: %s", t.responseText);
			var arr = t.responseText.split("\n");
			
			if (arr.length >= 2) {
				var device_id = arr[0].substr("DeviceId=".length);
				var device_key = arr[1].substr("DeviceKey=".length);
				
				// Hard coded for testing, these don't work
				/*
				if (this.deviceKeyNumTries == 0) {
					device_id = "AOuj_Rq-Hy_6dM06-QTC3wj5LXsTm3TznBApPKAlBlUTAC0Ks8jVlFwOE-3jEHSY0f2XBaYZsaFreiOwI8gqN0Nxk4GZfNX3YPSadNcnfOZHq1y7la0BwY0";
					device_key = "wsUrjmNHeaXZFpzQlDFqK+KklHuanIJRde9VNT+m/LY=";
				}
				*/
				
				device_key = AppAssistant.crypto.getDeviceAuth(device_key);
				this.testDeviceKeyIsValid({deviceId: device_id, deviceKey: device_key});
			}
		}
	},
	
	// There is a bug in crypto library where for some values of device key
	// decoding is not done correctly. I used python script in test/youtube_enc.py 
	// to verify that output coming from Crypto's rijndaelEncrypt function and
	// ouput from the python scrypt don't match in such instances.
	// When they don't match every API request to youtube will fail.
	// Althdough this happens rarely, the bug should be tracked down, 
	// or we should switch to using a different javascript library for aes encryption/decryption. 
	// Current workaround is to test if API request will succeed with obtained device key
	// and if not, re-register a device for a new set of device_key/device_id pair.
	// This solution is ok for now, since 
	// one: decoding error happens rarely
	// two: once we obtain the working device key, we store it and never need to register again.
	testDeviceKeyIsValid: function(authToken) { 
		Mojo.Log.info("Youtube: API testDeviceKeyIsValid");
		var url = 'http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&max-results=#{maxResults}&start-index=#{startindex}&time=today&alt=json&format=#{format}&safeSearch=strict'.
			interpolate({
            	maxResults: 1,
            	startindex: 1,
            	format: this.videoFormat
        	});
        AppAssistant.net.get(url, authToken, 
			this.deviceKeyIsValidCb.bind(this, authToken), this.deviceKeyIsNotValidCb.bind(this));
	},
	
	deviceKeyIsValidCb: function(authToken, result) {
		Mojo.Log.info("Youtube: API deviceKeyIsValidCb");
		Mojo.Log.info("Youtube: api deviceKeyIsValidCb, this.deviceKey decoded but base64 encoded: *%s*", Base64.encode(authToken.deviceKey));
		// store keys in globals for use by API functions
		this.authToken.deviceKey = authToken.deviceKey;
		this.authToken.deviceId = authToken.deviceId;

		if (this.candidateToken == null ||
			this.candidateToken.deviceKey !== authToken.deviceKey ||
			this.candidateToken.deviceId !== authToken.deviceId){

			// store device auth info in a cookie
			var obfuscatedKey = this.obfuscateKey(authToken.deviceKey);

			var cookie = new Mojo.Model.Cookie("youtube-dev");
			var storedCookie = cookie.get();
			if (storedCookie) {
				Mojo.Log.error("Youtube: Cookie existed, but was not right");
				cookie.remove();
			}
			cookie.put({reg1: authToken.deviceId, reg2: obfuscatedKey});

		} 				
	
	},
	
	deviceKeyIsNotValidCb: function(result) {
		Mojo.Log.info("Youtube: API deviceKeyIsNotValidCb");
		
		if (result.indexOf("Error 403") >= 0) {
			// re-register at this point to obtain a new set: device key and device id
			if (this.deviceKeyNumTries < 5) {
				this.deviceKeyNumTries++;
				this.getDeviceAuth();
			}
			else {
				this.registrationFailed = true;
				// Ask the user to try later
				Mojo.Controller.appController.showBanner(
					{messageText: "Registration failed. Try later."}, null);
			}
		}
		else {
			Mojo.Log.error("Youtube app API deviceKeyIsNotValidCb ERROR is not 403");
			this.registrationFailed = true;
			// Ask the user to try later
			Mojo.Controller.appController.showBanner(
				{messageText: "Registration failed. Try later."}, null);
		}
	},
	
	// garble device key before storing it
	obfuscateKey: function(deviceKey) {
		var key = "what's up duck!";
		var block = [];
	    for (var i = 0; i < deviceKey.length; i++) {
			if (i < key.length) {
				block[i] = deviceKey.charCodeAt(i) ^ key.charCodeAt(i);
			}
			else {
				block[i] = deviceKey.charCodeAt(i);
			}
	    }
		
		var result = "";
  		for(var i = 0; i < block.length; i++)
      		result += String.fromCharCode(block[i]);
			
		return result;
	},
	
	// after key is read from the cookie we need to deobfuscate it
	deobfuscateKey: function(deviceKey) {
		Mojo.Log.info("Youtube: api deobfuscateKey");
		return this.obfuscateKey(deviceKey);
	},
	
	// handle errors from youtube registerDevice api
	getDeviceAuthErrCb: function(t) {
		// TODO
	},
			
		
	/**********************************************
    * API functions
    ***********************************************/
	mostPopular: function(startIndex, numResults, callback) {
		if (this.registrationFailed) return;
			
		if (this.authToken.deviceKey == 0) {
			Mojo.Log.info("Youtube app API mostPopular: !!!!!!!!!!!!! SEMI_ERROR device key is 0, setting timeout to 1sec startIndex %d, numResults %d", startIndex, numResults);
			setTimeout(this.mostPopular.bind(this, startIndex, numResults, callback), this.NO_REGISTRATION_REPEAT_MS);
			return;
		}
					
        var url = 'http://gdata.youtube.com/feeds/api/standardfeeds/most_popular?v=2&max-results=#{maxResults}&start-index=#{startindex}&time=this_week&alt=json&format=#{format}&safeSearch=strict&client=#{clientId}&orderby=published'.
			interpolate({
            	maxResults: numResults,
            	startindex: startIndex,
            	format: this.videoFormat,
				clientId: this.authToken.clientId
        	});
		Mojo.Log.info("Youtube app API mostPopular: url %s", url);
        var cb = this.parseVideos.bind(this, callback);
        AppAssistant.net.get(url, this.authToken, cb);
    },
	
	mostViewed: function(startIndex, numResults, callback) {
		if (this.registrationFailed) return;
		
		if (this.authToken.deviceKey == 0) {
			Mojo.Log.info("Youtube app API mostViewed: !!!!!!!!!!!!! SEMI_ERROR device key is 0, setting timeout to 1sec startIndex %d, numResults %d", startIndex, numResults);
			setTimeout(this.mostViewed.bind(this, startIndex, numResults, callback), this.NO_REGISTRATION_REPEAT_MS);
			return;
		}
		
        var url = 'http://gdata.youtube.com/feeds/api/standardfeeds/most_viewed?v=2&max-results=#{maxResults}&start-index=#{startindex}&time=this_week&alt=json&format=#{format}&safeSearch=strict&client=#{clientId}&orderby=viewCount'.
			interpolate({
            	maxResults: numResults,
            	startindex: startIndex,
            	format: this.videoFormat,
				clientId: this.authToken.clientId
        	});
		Mojo.Log.info("Youtube app API mostViewed: url %s", url);
        var cb = this.parseVideos.bind(this, callback);
        AppAssistant.net.get(url, this.authToken, cb);
    },
	
    search: function(query, startIndex, numResults, callback){
		if (this.registrationFailed) return;
		
		if (this.authToken.deviceKey == 0) {
			Mojo.Log.info("Youtube app API search: !!!!!!!!!!!!! SEMI_ERROR device key is 0, setting timeout to 1sec startIndex %d, numResults %d", startIndex, numResults);
			setTimeout(this.search.bind(this, query, startIndex, numResults, callback), this.NO_REGISTRATION_REPEAT_MS);
			return;
		}
		
		query = encodeURIComponent(query);
		
		var url = 'http://gdata.youtube.com/feeds/api/videos?v=2&q=#{vq}&orderby=relevance&max-results=#{maxResults}&start-index=#{startindex}&alt=json&format=#{format}&safeSearch=strict&client=#{clientId}'.
			interpolate({
			    vq: query,
			    maxResults: numResults,
			    startindex: startIndex,
			    format: this.videoFormat,
				clientId: this.authToken.clientId
			});
		Mojo.Log.info("Youtube app API search: url %s", url);
		var cb = this.parseVideos.bind(this, callback);
		AppAssistant.net.get(url, this.authToken, cb);
    },
	
	searchByAuthor: function(query, startIndex, numResults, callback){
		if (this.registrationFailed) return;
		
		if (this.authToken.deviceKey == 0) {
			Mojo.Log.info("Youtube app API searchByAuthor: !!!!!!!!!!!!! SEMI_ERROR device key is 0, setting timeout to 1sec startIndex %d, numResults %d", startIndex, numResults);
			setTimeout(this.searchByAuthor.bind(this, query, startIndex, numResults, callback), this.NO_REGISTRATION_REPEAT_MS);
			return;
		}
		
		query = encodeURIComponent(query);
		
		var url = 'http://gdata.youtube.com/feeds/api/videos?v=2&author=#{vq}&orderby=relevance&max-results=#{maxResults}&start-index=#{startindex}&alt=json&format=#{format}&safeSearch=strict&client=#{clientId}'.
			interpolate({
			    vq: query,
			    maxResults: numResults,
			    startindex: startIndex,
			    format: this.videoFormat,
				clientId: this.authToken.clientId
			});
		Mojo.Log.info("Youtube app API searchByAuthor: url %s", url);
		var cb = this.parseVideos.bind(this, callback);
		AppAssistant.net.get(url, this.authToken, cb);
    },
	
	related: function(feedPath, startIndex, numResults, callback) {
		if (this.registrationFailed) return;
		
		if (this.authToken.deviceKey == 0) {
			Mojo.Log.info("Youtube app API related: !!!!!!!!!!!!! SEMI_ERROR device key is 0, setting timeout to 1sec startIndex %d, numResults %d", startIndex, numResults);
			setTimeout(this.related.bind(this, query, startIndex, numResults, callback), this.NO_REGISTRATION_REPEAT_MS);
			return;
		}
		
		var url = '#{base}&max-results=#{maxResults}&start-index=#{startindex}&alt=json&format=#{format}&safeSearch=strict'.
			interpolate({
			    //base: video.feed_path,
				base: feedPath,
			    maxResults: numResults,
			    startindex: startIndex,
			    format: this.videoFormat
			});
		Mojo.Log.info("Youtube app API related: url %s", url);
		var cb = this.parseVideos.bind(this, callback);
		AppAssistant.net.get(url, this.authToken, cb);
	},
	
	lookupVideo: function(id, callback, errCb) {
		if (this.registrationFailed) return;
		
		if (this.authToken.deviceKey == 0) {
			Mojo.Log.info("Youtube app API lookupVideo: !!!!!!!!!!!!! SEMI_ERROR device key is 0, setting timeout to 1sec video ID %s", id);
			setTimeout(this.lookupVideo.bind(this, id, callback, errCb), this.NO_REGISTRATION_REPEAT_MS);
			return;
		}
		
		var url = 'http://gdata.youtube.com/feeds/api/videos/#{videoId}?v=2&alt=json&format=#{format}&client=#{clientId}&safeSearch=strict'.
			interpolate({
				videoId: id,
			    format: this.videoFormat,
				clientId: this.authToken.clientId
			});
		Mojo.Log.info("Youtube app API lookupVideo: url %s", url);
		var cb = this.parseVideo.bind(this, callback);
		AppAssistant.net.get(url, this.authToken, cb, errCb);
	},
	
	
	/**********************************************
    * Parsing functions
    ***********************************************/
    
    // Processes youtube video listing response; sends null on failure
    parseVideos: function(callback, result) {
        if (!result) {
            Mojo.Log.info('Parse videos failed - null result');
            callback(null);
            return;
        }
        
        var json;
        try {
            json = result.evalJSON(true);
        } 
        catch (e) {
            Mojo.Log.info('Parse videos failed - bad json: ' + e);
            callback(null);
            return;
        }
		
		if (!json.feed.entry){
			Mojo.Log.info('Parse videos failed results list is empty');
            callback(null, size);
            return;
		}
		
        var size = json.feed["openSearch$totalResults"]["$t"];
        if (json.feed.openSearch$totalResults.$t < 1) {
            callback(null);
            return;
        }
        
        var mVideos = new Array();
        for (var i = 0; i < json.feed.entry.length; i++) {
			mVideos[i] = this._parseVideo(json.feed.entry[i]);
        }
      
        callback(mVideos, size);
    },
	
	// Processes youtube video lookup response; sends null on failure
    parseVideo: function(callback, result) {
        if (!result) {
            Mojo.Log.info('Parse video failed - null result');
            callback(null);
            return;
        }
        
        var json;
        try {
            json = result.evalJSON(true);
        } 
        catch (e) {
            Mojo.Log.info('Parse video failed - bad json: ' + e);
            callback(null);
            return;
        }
		
		if (!json.entry){
			Mojo.Log.info('Parse video failed results video info is empty');
            callback(null);
            return;
		}
				
		var video = this._parseVideo(json.entry);
        callback(video);
    },
	
	// parses a single video and returns it
	_parseVideo: function(entry) {
		//Mojo.Log.info('_parseVideo entry %j', entry);
        var min, sec = entry.media$group.yt$duration.seconds;
        min = Math.floor(sec / 60);
        sec %= 60;
        if (sec < 10) {
            // pad 0
            sec = '0' + sec;
        }
    			
		var relatedURL = "";
		if (entry.link) {
			var linkIndex;
			for (linkIndex = 0; linkIndex < entry.link.length; linkIndex++) {
				if (entry.link[linkIndex].rel.indexOf("video.related") >= 0 ) {
					relatedURL = entry.link[linkIndex].href;
					break;
				}
			}
		}		
		
		// extract thumbnails
		var thumbSmall = "";
		var thumbLarge = "";
		
		if (entry.media$group && entry.media$group.media$thumbnail && 
			entry.media$group.media$thumbnail.constructor == Array && entry.media$group.media$thumbnail.length > 0 )
		{
			thumbLarge = thumbSmall = entry.media$group.media$thumbnail[0].url;
			for (var index = 0; index < entry.media$group.media$thumbnail.length; index++) {
				// for higher quality thumbnail we select the first one 
				// with height greater than 200px
				// images with height > 400px might take too long to pull down
				// so we opt for a small stretched thumbnail
				if (entry.media$group.media$thumbnail[index].height > 200
				 	&& entry.media$group.media$thumbnail[index].height < 400) 
				{
					thumbLarge = entry.media$group.media$thumbnail[index].url;
					break;
				}
			}
		}
		
		return {
			id: entry.media$group ? (entry.media$group.yt$videoid ? entry.media$group.yt$videoid.$t : "") : "",
            title: entry.media$group ? (entry.media$group.media$title ? entry.media$group.media$title.$t : "") : "",
            author: entry.author.constructor == Array ? (entry.author[0].name ? entry.author[0].name.$t : "") : "",
            rating: entry.gd$rating ? "" + Math.round(entry.gd$rating.average) : "",
            views: entry.yt$statistics ? entry.yt$statistics.viewCount : 0,
            duration: min + ':' + sec,
            desc: entry.media$group ? (entry.media$group.media$description ? entry.media$group.media$description.$t : "") : "",
			thumbnail: thumbSmall,
			thumbnail_240_320: thumbLarge,
			link: entry.media$group ? (entry.media$group.media$content ? 
				(entry.media$group.media$content.constructor == Array && entry.media$group.media$content.length > 0 ?
				entry.media$group.media$content[0].url : "") : "") : "",
				
			shareLink: entry.media$group ? (entry.media$group.media$player ?
				(entry.media$group.media$player.constructor == Array && entry.media$group.media$player.lenght > 0 ? 
				entry.media$group.media$player[0].url : entry.media$group.media$player.url) : "") : "",
            
			related_url: relatedURL,
        };
	},
	
	
	/**********************************************
    * Global data
    ***********************************************/
	
	// Passed to functions in net.js to provide appropriate authorization headers
    authToken: {
        //key: auth_key,
        develKey: "AI39si5_wx34IKk2v4PEhKjw4UdqYHVQ_HH_db6hDbhtKVUJBvZkSMBf1Zdbt3grBGjusRChIsOmrDR1GaiAw5ECsjc9DLLzKg",
        clientId: "ytapi-PalmInc-PalmPre", //"ytapi-PalmInc-YouTubeforPalm-na30lkrg-0",
        deviceKey: 0,
        deviceId: 0,
		deviceSerialNum : "12345",
    },

	// TODO, do I ever need format 2?
	// 2 - 3gp over http
	// 3 - mp4 over http (H.264)
	videoFormat : 3,
	
	// URLs
	registerDeviceURL: "http://www.google.com/youtube/accounts/registerDevice",

});
