var SYSTEMINFO, UID;

RoaringApps = angular.module('RoaringAppsMac', ['ra.bridge-service', 'ra.firebase-service', 'ra.account-service', 'ra.storage-service', 'ra.directories-service', 'ra.local-apps-service', 'ra.app-match-service', 'cfp.hotkeys', 'firebase', 'autoheight'])

.config(function(hotkeysProvider) {
	hotkeysProvider.includeCheatSheet = false;
	
	// Initialize Firebase
	  var config = {
	    apiKey: "AIzaSyBNeBcBMrzTJlkumMy3NzrS4K8DPTWQucs",
	    authDomain: "roaringapps.firebaseapp.com",
	    databaseURL: "https://roaringapps.firebaseio.com",
	    projectId: "project-6364918956167083622",
	    storageBucket: "",
	    messagingSenderId: "160959248842"
	  };
	  firebase.initializeApp(config);
		
		window.FirebaseAuth = firebase.auth();
		window.FirebaseRoot = firebase.database().ref();
});
angular.module('ra.account-service', ['ra.storage-service'])

.service('AccountService', function(Storage, $q) {
	var provider = this;
	
	provider.auth = undefined;
		
	provider.firebase = {
		rootConnection: window.FirebaseRoot
	};
	
	//listeners for a specific event
	provider.eventListeners = {};
	provider.authListeners = [];
	
	provider._init = function() {
				
		provider.auth = window.FirebaseAuth;
				
		provider._startSimpleLogin();
		provider._checkForStoredToken();
	};
	
	provider.login = function(email, password) {		
		
		var deferred = $q.defer();
				
		provider.auth.signInWithEmailAndPassword(email, password)
		.then(function(r) {	
			console.log('Sign in service successful', r);
			deferred.resolve(true);
		})
		.catch(function(error) {			
			if(error) {
				console.log('Sign in service error', error);
				deferred.reject(error);
			}
		});
		
		return deferred.promise;
	};
	
	provider.signout = function() {
		provider.auth.signOut();
		provider._fire('user', null);
		Storage.remove('firebaseAuthToken');
	};
	
	//add event listener for specific message
	provider.on = function(event, callback) {
		if(provider.eventListeners.hasOwnProperty(event)) {
			provider.eventListeners[event].push(callback);
		}
		else {
			provider.eventListeners[event] = [callback];
		}
	};
		
	provider._startSimpleLogin = function() {
		
		provider.auth.onIdTokenChanged(function(authData) {			
						
			//user authenticated
			if(authData) {
				Storage.set('firebaseAuthToken', authData.refreshToken);
				
				provider._fireAuth({success: true});
				provider._loggedIn(authData);
			}
			//else user is logged out
			else {
				provider._fire('user', null);
			}
		}, function(error) {
				provider._fireAuth({message: 'An error occurred authenticating your account.'});
		});
	};
	
	provider._checkForStoredToken = function() {
		if(Storage.has('firebaseAuthToken')) {
			var token = Storage.get('firebaseAuthToken');
			
			//todo: sign in with token??? Not sure if we need to use the refresh token
				//or if it is handled adequately by the firebase auth SDK
			
			/*
			provider.firebase.rootConnection.auth(token, function(error, success) {
				if(error) {
					console.log("Login Failed!", error);
					provider.signout();
					
					//tells the user they need to sign in again...
					provider._fire('expired_token', null);
				} else {
					provider._loggedIn(success.auth);
				}
			});			
			*/
		}
	};
	
	provider._loggedIn = function(authData) {
		
		var ref, id;
		
		if(authData.hasOwnProperty('uid')) {
			ref = 'uids';
			id = authData.uid;
		}
		else if (authData.hasOwnProperty('id')) {
			ref = 'ids';
			id = authData.id;
		}
		else {
			ref = 'emails';
			id = authData.email;
		}
		
		provider._findUsername(ref, id, function(username) {
			provider._findUserRecord(username, function(record) {				
				provider._fire('user', record);
				
				provider._findUserProfile(username, function(profile) {					
					//identify user in mixpanel
					mixpanel.people.set({
						'$first_name': username,
						username: username
					});
				});
				
				
				/*
				//get this user's status reports
				Account.firebase.userStatusReports = provider.firebase.rootConnection.child('status_reports_by_user').child(user.username);
				Account.firebase.userStatusReports.on('value', function(snap) {
					if(snap.val()) {
						Storage.set('user_status_reports', snap.val());
						
						Apps.statusReports = snap.val();
						
						//todo: update react 
						Apps.filter();
					}
				});
				*/
			});
		});
	};
	
	provider._findUsername = function(ref, id, callback) {
		
		provider.firebase.rootConnection.child('providers').child(ref).child(provider._encodeReference(id)).once('value', function(snap) {
			if(!snap.val()) {
				console.log('Error getting username.');
				callback(null);
			}
			else {
				callback(snap.val());
			}
		});
	};
	
	provider._findUserRecord = function(username, callback) {
		
		provider.firebase.rootConnection.child('users').child(provider._encodeReference(username)).once('value', function(snap) {
			if(!snap.val()) {
				console.log('Error getting user record');
				callback(null);
			}
			else {
				callback(snap.val());
			}
		});
	};
	
	provider._findUserProfile = function(username, callback) {
		
		provider.firebase.rootConnection.child('user_profiles').child(provider._encodeReference(username)).once('value', function(snap) {
			if(!snap.val()) {
				console.log('Error getting user profile.');
				callback(null);
			}
			else {
				callback(snap.val());
			}
		});
	};
	
	provider._encodeReference = function(t) {
		return t.replace(/\./g,",").replace(/[$\[\]#\/]/g,"")
	};
	
	//fire an event
	provider._fire = function(event, payload) {
		//all listeners
		/*
		for (var i = 0; i < provider.listeners.length; i++) {
			var f = provider.listeners[i];
			f(payload);
		}
		*/	
		//specific listeners
		if(provider.eventListeners.hasOwnProperty(event)) {			
			for (var c = 0; c < provider.eventListeners[event].length; c++) {
				var fn = provider.eventListeners[event][c];
				fn(payload);
			}
		}
	};
	
	provider._fireAuth = function(payload) {
		for (var c = 0; c < provider.authListeners.length; c++) {
			var fn = provider.authListeners[c];
			fn(payload);
		}
	};
	
	provider._init();
	
	return provider;
});
angular.module('ra.app-match-service', ['ra.storage-service'])

.service('AppMatchService', function(Storage, $q, $timeout) {
	var provider = this;
	
	/* Variables */
	provider.allApps = null;
	provider.localApps = null;
	provider.matchedApps = [];
	
	/* Firebase */
	provider.rootConnection = window.FirebaseRoot;
		
	/* Helper functions */
	provider.encodeReference = function(t){return t.replace(/\./g,",").replace(/[$\[\]#\/]/g,"")};
	provider.decodeReference = function(t){return t.replace(/\,/g,".")};
	provider.validEmail = function(t){return!t.val()||Boolean(t[0].checkValidity)&&!t[0].checkValidity()?!1:!0};
	provider.randomInt = function(min, max) {return Math.floor(Math.random() * (max - min + 1)) + min;};
	//src: http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
	provider.makeUniqueId = function() {
	  function s4() {
	    return Math.floor((1 + Math.random()) * 0x10000)
	      .toString(16)
	      .substring(1);
	  }
	  return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
	    s4() + '-' + s4() + s4() + s4();
	};
	
	/* Internal/private functions */
	provider._getRemoteApps = function() {
		var deferred = $q.defer();
		
		//todo: check to see when last fetched, don't fetch again within ~30 mins...(or something, maybe check when last status was reported)
		
		$.ajax({
			type: 'GET',
			url: 'https://s3.amazonaws.com/s3.roaringapps.com/data/v3/all.json',
			dataType: 'json',
			xhrFields: {
				onprogress: function(e) {
					if (e.lengthComputable) {
						//service._appsDownloadProgress(e.loaded, e.total);
					}
					else {
						//service._appsDownloadProgress(e.loaded, 0);
					}
				}
			},
			success: function(data) {
				provider.allApps = data.apps;
				//Storage.set('v2:remoteApps', data.apps);
				Storage.set('v2:lastFetch', moment().unix());	
				
				deferred.resolve(true);			
				
				//$rootScope.$broadcast('apps:loaded', data.apps);
			},
			error: function(xhr, error, error2) {
				//todo: test this
				deferred.reject(error);
			}
		});
		
		return deferred.promise;
	};
	
	provider._matchApps = function() {
		var deferred = $q.defer();
		
		provider.matchedApps = []; //reset
				
		var requestApps = provider.localApps,
			count = requestApps.length,
			
			matchAppsSelfTimeout = setTimeout(function() {
										console.log(moment().format() + ': matchApps timed out after 60 seconds.');
										deferred.reject('matchApps timed out'); //todo: test...
									}, 1000 * 60);
			
		function processApp(i) {
			var theApp = requestApps[i],
					theBundleId = theApp.bundleId.toLowerCase(),
				matched = [];
			
			/*
			 * different search criteria
			 */
			 
			 //console.log('Processing app', theApp, provider.allApps);
			
			//exact bundleId match. certainty = 0.99
			//todo: ...
//			var matches0 = _.filter(provider.allApps, function(filterApp){ 
//				var bundleIdExists = filterApp.hasOwnProperty('platforms') 
//								&& filterApp.platforms.indexOf('osx') > -1  
//								&& filterApp.hasOwnProperty('bundle_id_osx');
//							
//				if(bundleIdExists) {
//					var bundleIdsLower = filterApp.bundle_id_osx.map(function(val) {return val.toLowerCase()});
//					var doesMatch = bundleIdsLower.indexOf(theApp.bundleId.toLowerCase());
//					console.log('bundleId', filterApp.bundle_id_osx, theApp.bundleId, doesMatch);
//				}
//				else if (typeof bundleIdProperty == 'string') {
//					var doesMatch = bundleIdProperty.toLowerCase() == theApp.bundleId.toLowerCase() ? 1 : -1;
//				}
//				else {
//					var doesMatch = -1;
//				}
//								
//				var alreadyMatched = matched.indexOf(filterApp.unique_id);
//				
//				if(alreadyMatched >= 0 && doesMatch >= 0) {
//					matched[alreadyMatched]._matchCertainty += 99;
//				}
//				else if(doesMatch >= 0) {
//					matched.push(filterApp.unique_id);
//					filterApp._matchCertainty = 99;
//					return true;
//				} 
//				else return false;
//			});

			var matches0 = _.filter(provider.allApps, function(filterApp) {
				return _.some(filterApp.bundle_id_osx, function(filterBundleId) {
					var doesMatch = filterBundleId.toLowerCase() === theBundleId ||
									(filterBundleId.toLowerCase() + '-setapp') === theBundleId;
			
					var alreadyMatched = matched.indexOf(filterApp.unique_id);
					
					if (alreadyMatched >= 0 && doesMatch) {
						matched[alreadyMatched]._matchCertainty += 98;
					} else if (doesMatch) {
						matched.push(filterApp.unique_id);
						filterApp._matchCertainty = 96;
						return true;
					} else {
						return false;
					}
				});
			});

						
			//exact name match. certainty = 0.9
			var matches1 = _.filter(provider.allApps, function(filterApp){ 
				var doesMatch = filterApp.hasOwnProperty('platforms') 
								&& filterApp.platforms.indexOf('osx') > -1  
								&& filterApp.hasOwnProperty('name') 
								&& filterApp.name.toLowerCase() == theApp.name.toLowerCase();
				var alreadyMatched = matched.indexOf(filterApp.unique_id);
				
				if(alreadyMatched >= 0 && doesMatch) {
					matched[alreadyMatched]._matchCertainty += 99;
				}
				else if(doesMatch) {
					matched.push(filterApp.unique_id);
					filterApp._matchCertainty = 95;
					return true;
				} 
				else return false;
			}),
						
			//name contained in name. certainty = 0.5
			matches2 = _.filter(provider.allApps, function(filterApp){ 
				var doesMatch = filterApp.hasOwnProperty('platforms') 
								&& filterApp.platforms.indexOf('osx') > -1  
								&& filterApp.hasOwnProperty('name') 
								&& filterApp.name.toLowerCase().indexOf(theApp.name.toLowerCase()) > -1;
				var alreadyMatched = matched.indexOf(filterApp.unique_id);
				
				if(alreadyMatched >= 0 && doesMatch) {
					matched[alreadyMatched]._matchCertainty += 50;
				}
				else if(doesMatch) {
					matched.push(filterApp.unique_id);
					filterApp._matchCertainty = 50;
					return true;
				} 
				else return false;
			});
			
			/*
			//developer contained in bundleId. certainty = 0.4
			matches3 = _.filter(scope.workers.appListener.allApps, function(filterApp){ 
				var doesMatch = filterApp.hasOwnProperty('platforms') 
								&& filterApp.platforms.indexOf('osx') > -1  
								&& theApp.hasOwnProperty('bundleId')
								&& filterApp.hasOwnProperty('developer') 
								&& theApp.bundleId.toLowerCase().indexOf(filterApp.name.toLowerCase().replace(/[\s]/g, '')) > -1;
				var alreadyMatched = matched.indexOf(filterApp.unique_id);
				
				if(alreadyMatched >= 0 && doesMatch) {
					matched[alreadyMatched]._matchCertainty += 40;
				}
				else if(doesMatch) {
					matched.push(filterApp.unique_id);
					filterApp._matchCertainty = 40;
					return true;
				} 
				else return false;
			});
			*/
			//current version contained in list of versions. certainty = 0.3
			
			
						
			theApp.matches = _.union(matches0, matches1, matches2);
			
			theApp.matches = _.sortBy(theApp.matches, '_matchCertainty');
			
			theApp.matches.reverse();
			
			provider.matchedApps.push(theApp);
			
			console.log('theApp.matches', theApp.matches, theApp);
			
			//proceed or finish
			if(i < count - 1) processApp(i + 1);
			else {
				clearTimeout(matchAppsSelfTimeout);
				deferred.resolve(true);
			}
		};
		
		processApp(0);
		
		return deferred.promise;
	};
	
	/* Public functions */
	provider.matchApps = function(localApps) {
		var deferred = $q.defer();
		
		provider.localApps = localApps;
		
		//1: get remote apps
		provider._getRemoteApps().then(function(success) {
			console.log('public fx has remote apps', provider.allApps);
			
			//2: match apps
			provider._matchApps().then(function(success) {
				console.log('_matchApps has finished', success, provider.matchedApps);
				//save locally
				Storage.set('matchedApps', provider.matchedApps);
				//resolve
				deferred.resolve(provider.matchedApps);
			
			//error matching apps
			}, function(error) {
				if(error) deferred.resolve(null, error);
			});
		//error getting remote apps
		}, function(error) {
			if(error) deferred.resolve(null, error);
		});
		
		return deferred.promise;
	};
	
	
	return provider;
});
angular.module('ra.bridge-service', [])

.service('Bridge', function() {
	var provider = this;
	
	provider.bridgeReady = false;
	
	//If a call to the bridge occurs before the bridge is ready,
	//it is queued here
	provider.sendQueue = [];
	
	//list of event listeners (functions)
	//each function will be called every time a new message is received
	provider.listeners = [];
	
	//listeners for a specific event
	provider.eventListeners = {};
	
	provider._init = function() {
		this._connectBridge(this._onBridgeConnected);
	};
	
	provider._connectBridge = function(callback) {
		if (window.WebViewJavascriptBridge) {
			callback(WebViewJavascriptBridge)
		} 
		else {
			document.addEventListener('WebViewJavascriptBridgeReady', function() {
				callback(WebViewJavascriptBridge);
			}, false);
		}
	};
	
	provider._onBridgeConnected = function(webviewBridge) {
		
		provider.bridgeReady = true;
		
		if(provider.sendQueue.length) provider._emptySendQueue();
		
		//webviewBridge.init() function receives all msgs from the bridge
		webviewBridge.init(function(message, responseCallback) {
			provider._fire(message.messageType, message.messageData);
		});
	};
	
	provider._emptySendQueue = function() {
		var count = provider.sendQueue.length;
		
		for (var i = 0; i < count; i++) {
			var obj = provider.sendQueue[i];
			provider.send(obj.data, obj.callback);
		}
		
		//reset queue
		provider.sendQueue = [];
	};
	
	provider._fire = function(event, payload) {
		//all listeners
		for (var i = 0; i < provider.listeners.length; i++) {
			var f = provider.listeners[i];
			f(payload);
		}
			
		//specific listeners
		if(provider.eventListeners.hasOwnProperty(event)) {			
			for (var c = 0; c < provider.eventListeners[event].length; c++) {
				var fn = provider.eventListeners[event][c];
				fn(payload);
			}
		}
	};
	
	//add event listener for every event
	provider.all = function(callback) {
		provider.listeners.push(callback);
	};
	
	//add event listener for specific event/messageType
	provider.on = function(event, callback) {
		if(provider.eventListeners.hasOwnProperty(event)) {
			provider.eventListeners[event].push(callback);
		}
		else {
			provider.eventListeners[event] = [callback];
		}
	};
	
	provider.send = function(data, callback) {
				
		if(provider.bridgeReady) {
			if(callback) {
				WebViewJavascriptBridge.send(data, callback);
			}
			else {
				WebViewJavascriptBridge.send(data);
			}		
		}
		//else queue it for later...
		else {
			provider.sendQueue.push({data: data, callback: callback});
		}
	};
	
	provider._init();
	
	return provider;
});
angular.module('ra.directories-service', [])

.service('Directories', function(Storage) {
	var provider = this;
	
	provider.defaultDirectories = ['/Applications/'];
	provider.currentDirectories = undefined;
	
	provider.init = function() {		
		if(Storage.has('directories') && Storage.get('directories')) {
			provider.currentDirectories = Storage.get('directories');
			provider.update();
		}
		else {
			provider.setDefaultDirectories();
		}
	};
	
	provider.setDefaultDirectories = function() {
		provider.currentDirectories = provider.defaultDirectories;
		provider.update();		
	},
	
	provider.add = function(dirs) {
		dirs.map(function(dir) {
			//bug on 10.9 - directories start with 'localhost', so remove that
			if(dir.indexOf('localhost/') === 0) dir = dir.substr(9);
			
			provider.currentDirectories.push(dir);
		});
		
		provider.update();
	};
	
	provider.remove = function(dir) {
		provider.currentDirectories.splice(provider.currentDirectories.indexOf(dir), 1);
		
		provider.update();		
	};
	
	provider.update = function() {
		Storage.set('directories', provider.currentDirectories);
	};
	
	
	provider.init();
	
	return provider;
	
});
angular.module('ra.firebase-service', ['ra.storage-service'])

.service('FirebaseService', function(Storage, $q, $timeout) {
	var provider = this;
	
	provider.rootConnection = window.FirebaseRoot;
	provider.tasksQueueRef = provider.rootConnection.child('server/queue/tasks');
	
	provider._init = function() {
		
	};
		
	provider.encodeReference = function(t){return t.replace(/\./g,",").replace(/[$\[\]#\/]/g,"")};
	provider.decodeReference = function(t){return t.replace(/\,/g,".")};
	provider.validEmail = function(t){return!t.val()||Boolean(t[0].checkValidity)&&!t[0].checkValidity()?!1:!0};
	provider.randomInt = function(min, max) {return Math.floor(Math.random() * (max - min + 1)) + min;};
	//src: http://stackoverflow.com/questions/105034/create-guid-uuid-in-javascript
	provider.makeUniqueId = function() {
	  function s4() {
	    return Math.floor((1 + Math.random()) * 0x10000)
	      .toString(16)
	      .substring(1);
	  }
	  return s4() + s4() + '-' + s4() + '-' + s4() + '-' +
	    s4() + '-' + s4() + s4() + s4();
	};
	
	provider.checkValidBeta = function(version, callback) {
		var version = version.replace(/\./g, ',');
		
		provider.rootConnection.child('admin/macapp/beta/versions').child(version).on('value', function(snap) {
			if(!snap.val()) {
				//no version recorded
				callback(false, 'invalid');
			}
			else if (snap.val() == 'valid') {
				//beta is valid
				callback(true, 'valid');
			}
			else if (snap.val() == 'superseded') {
				//beta has expired, show splash screen
				callback(false, 'superseded');
			}
			else if (snap.val() == 'expired') {
				//beta has expired, show splash screen
				callback(false, 'expired');
			}
		});
	};
	
	provider.checkForNewerAppVersion = function(version, callback) {		
		provider.rootConnection.child('admin/macapp/latest-version').on('value', function(snap) {
			if(!snap.val()) {
				callback(null);
			}
			else {
				var newerVersion = VersionCompare.gt(snap.val(), version);
				if(newerVersion) callback(snap.val());
				else callback(null);	
			}
		});
	};
	
	provider.logEvent = function(data) {
		data.createdAt = moment().unix();
		data.uid = UID || null;
		
		provider.rootConnection.child('admin/macapp/logging/analytics').push(data);
	};
	
	provider.logError = function(data) {
		data.createdAt = moment().unix();
		data.uid = UID || null;
		
		provider.rootConnection.child('admin/macapp/logging/errors').push(data);
	};
	
	provider.logPerformanceMetrics = function() {
		
		try {
			window.performance.measure('appFetchLocal', 'fetchApps_local_start', 'fetchApps_local_received');
			window.performance.measure('appFetchRemote', 'fetchApps_remote_start', 'fetchApps_remote_received');
			window.performance.measure('appFetchTotal', 'fetchApps_local_start', 'fetchApps_remote_received');
					
			provider.logEvent({
				type: 'metrics', 
				appFetchLocal: window.performance.getEntriesByName('appFetchLocal')[0].duration || null,
				appFetchRemote: window.performance.getEntriesByName('appFetchRemote')[0].duration || null,
				appFetchTotal: window.performance.getEntriesByName('appFetchTotal')[0].duration || null,
				os: SYSTEMINFO.osNumber || null,
				mac: SYSTEMINFO.macModel || null
			});
		}
		catch (error) {
			console.log('Error measuring performance.', error);
		}
	};
	
	provider.createServerTask = function(task, data) {
		data._state = task;
		data.created_at = new Date().toString();
		
		provider.tasksQueueRef.push(data);
	};
	
	provider.connectionListener = function() {
		var deferred = $q.defer();
		
		provider.rootConnection.child('.info/connected').on('value', function(snap) {
			deferred.notify(snap.val());
		});
		
		return deferred.promise;
	}
	
	provider.sendApps = function(data, callback) {		
		window.performance.mark('fetchApps_remote_start');
		
		//Interface.updateProgressDescription('Compiling installed applications...');
		
		
		//Storage.set('directories', Directories.currentDirectories);
		
		//sends list of local apps to firebase server
		//listens for changes to the queue for the result
		var obj = {
			apps: data,
			uid: provider.makeUniqueId(),
			_state: 'app_match_start'
		};
		var queueRef = provider.rootConnection.child('server/queue/tasks').push(obj),
			responseRef = provider.rootConnection.child('server/responses').child(obj.uid),
			numReceived = 0,
			receivedApps = [],
			incompleteTimeout;
		
		incompleteTimeout = $timeout(function() {
								if(receivedApps.length) {
									provider.logError({
										type: 'remote-fetch-timeout-partial',
										error: 'Fetching remote apps timed out. Completed with ' + receivedApps.length + ' received apps.',
										apps: data
									});
									
									console.log('Timed out, completing with haveApps.');
									haveApps();	
								}
								else {
									
									provider.logError({
										type: 'remote-fetch-timeout',
										error: 'Fetching remote apps timed out. No apps were received.',
										apps: data
									});
									
									console.log('Timed out, no apps received yet.');
									if(callback) callback(null, true);
								}
							}, 1000 * 70);
		
		
		
		//when _state == 'app_match_delivered', we can save the `apps` 
		//then update the task to tell the server to remove it
		
		responseRef.on('child_added', function(snap) {
			if(snap.val()) {
				
				numReceived++;
				receivedApps.push(snap.val());
				
				if(numReceived == obj.apps.length) {
					console.log('Have all remote apps, proceeding.');
					$timeout.cancel(incompleteTimeout);
					haveApps();
				}
				
			}
		});
		
		function haveApps() {
			Storage.set('remoteApps', receivedApps);
			Storage.set('lastFetch', moment().unix());
			
			window.performance.mark('fetchApps_remote_received');
			
			provider.logPerformanceMetrics();
			
			//process apps
			//Apps.processApps(receivedApps);
			if(callback) callback(receivedApps);
						
			queueRef.update({
				_state: 'app_match_received', 
				_state_changed: new Date().getTime(),
				uid: obj.uid
			});
		};
	};
	
	return provider;
});
angular.module('ra.local-apps-service', ['ra.bridge-service', 'ra.directories-service', 'ra.storage-service', 'ra.firebase-service'])

.service('LocalAppsService', function(Bridge, Directories, Storage, $q, $timeout, FirebaseService) {
	var provider = this;
			
	provider._init = function() {
		
	};
	
	provider.settings = {
		maxFetchTime: 60
	};
			
	provider.getApps = function() {
		var deferred = $q.defer();		
		window.performance.mark('fetchApps_local_start');
		
		//Start a timer. If we haven't received the local apps after 60 secs, something went wrong...
		var localAppsTimeout = $timeout(function() {
			console.log(moment().format() + ': fetching local apps from Bridge timed out after 60 seconds.');
			deferred.reject('timed-out');
			
			FirebaseService.logError({
				type: 'localapps-fetch-timeout',
				error: 'Fetching local apps via Bridge timed out.'
			});
		}, 1000 * provider.settings.maxFetchTime);
		
				
		//get list of apps installed locally
		Bridge.send({messageType: 'locateApps', messageData: Directories.currentDirectories}); 
		Bridge.on('appsList', function(resp) {
			
			window.performance.mark('fetchApps_local_received');
			
			$timeout.cancel(localAppsTimeout);
			
			//save locally
			Storage.set('localApps', resp);
			
			deferred.resolve(resp);
		});
		
		return deferred.promise;
	};
	
	return provider;
});
angular.module('ra.storage-service', [])

.service('Storage', function() {
	var provider = this;
	
	provider.set = function(key, val) {
		//if(typeof val == 'object') val = JSON.stringify(val);
		try {
			localStorage.setItem(key, JSON.stringify(val));
		}
		catch (error) {
			console.log('Error saving to local storage.', error);
		}
	};
	
	provider.get = function(key) {
		var item = localStorage.getItem(key);
		
		return JSON.parse(item);
	};
	
	provider.remove = function(key) {
		localStorage.removeItem(key);
	};
	
	provider.has = function(key) {
		var item = localStorage.getItem(key);
		
		if(item !== null) return true;
		else return false;
	};
	
	provider.nuke = function() {
		localStorage.clear();
	};
	
	return provider;
});
//angular filters

(function() {
	
	var hideSystemAppsFilter = function() {
		return function(apps, filterActive) {
			
			var systemApps = [
				"com.apple.ActivityMonitor",
				"com.apple.airport.airportutility",
				"com.apple.appstore",
				"com.apple.ScriptEditor2",
				"com.apple.Automator",
				"com.apple.bootcampassistant",
				"com.apple.calculator",
				"com.apple.iCal",
				"com.apple.Chess",
				"com.apple.Console",
				"com.apple.AddressBook",
				"com.apple.DVDPlayer",
				"com.apple.dashboardlauncher",
				"com.apple.Dictionary",
				"com.apple.DiskUtility",
				"com.apple.Disk-Utility",
				"com.apple.DigitalColorMeter",
				"com.apple.FaceTime",
				"com.apple.FontBook",
				"com.apple.gamecenter",
				"com.apple.Grab",
				"com.apple.grapher",
				"com.apple.Image_Capture",
				"com.apple.keychainaccess",
				"com.apple.launchpad.launcher",
				"com.apple.mail",
				"com.apple.Maps",
				"com.apple.iChat",
				"com.apple.exposelauncher",
				"com.apple.Notes",
				"com.apple.PhotoBooth",
				"com.apple.Photos",
				"com.apple.Preview",
				"com.apple.QuickTimePlayerX",
				"com.apple.reminders",
				"com.apple.Safari",
				"com.apple.Stickies",
				"com.apple.systempreferences",
				"com.apple.Terminal",
				"com.apple.TextEdit",
				"com.apple.backup.launcher",
				"com.apple.X11redirect",
				"com.apple.iBooksX",
				"com.apple.iTunes",
				"com.apple.VoiceOverUtility",
				"com.apple.audio.AudioMIDISetup",
				"com.apple.ColorSyncUtility",
				"com.apple.SystemProfiler",
				"com.apple.MigrateAssistant",
				"com.apple.siri.launcher"
			];
			
			if(!filterActive) return apps;
			else {
				var filtered = [];
				angular.forEach(apps, function(app) {
					if(systemApps.indexOf(app.bundleId) === -1) filtered.push(app);
				});
				return filtered;
			}		
		};
	};
	
	var filterByStatusFilter = function() {
		return function(apps, settings) {
						
			if(settings.status == 'All') return apps;
			else if (settings.status == 'Unknown') {
				var filtered = [];
				angular.forEach(apps, function(app) {
					if(app.hasOwnProperty('matches') && app.matches.length && (!app.matches[0].hasOwnProperty('statuses') || !app.matches[0].statuses.hasOwnProperty('osx') || !app.matches[0].statuses.osx.hasOwnProperty(settings.os)) ) {
						filtered.push(app);
					}
				});
				return filtered;
			}
			else {
				var filtered = [];
				angular.forEach(apps, function(app) {
					if(app.hasOwnProperty('matches') && app.matches.length  && app.matches[0].hasOwnProperty('statuses') && app.matches[0].statuses.osx.hasOwnProperty(settings.os)) {
						if(app.matches[0].statuses.osx[settings.os].allVersions.result == settings.status) {
							filtered.push(app);
						}
					}
				});
				return filtered;
			}
		
		};		
	};
	
	var filterByMatches = function() {
		return function(apps, numOfMatches) {
						
			if (numOfMatches > 0) {
				var filtered = [];
				angular.forEach(apps, function(app) {
					if(app.hasOwnProperty('matches') && app.matches.length >= numOfMatches) {
						filtered.push(app);
					}
				});
				return filtered;
			}
			else if (numOfMatches === 0) {
				var filtered = [];
				angular.forEach(apps, function(app) {
					if(!app.hasOwnProperty('matches') || app.matches.length === 0) {
						filtered.push(app);
					}
				});
				return filtered;
			}
		
		};
	};
	
	var filterByFavourites = function() {
		return function(apps, favourites) {
		
			if(!favourites) return apps;
			else {
				return _.filter(apps, function(app) {
							if(app.hasOwnProperty('matches') && app.matches.length > 0) {
								return favourites.hasOwnProperty(app.matches[0].slug);
							}
							else return false;
						});
				
			}
			
		}
	};
	
	RoaringApps.filter('hideSystemApps', hideSystemAppsFilter);
	RoaringApps.filter('filterByStatus', filterByStatusFilter);
	RoaringApps.filter('filterByMatches', filterByMatches);
	RoaringApps.filter('filterByFavourites', filterByFavourites);
	
})();
//console.log('maincontroller...', RoaringApps);

(function() {
	var MainCtrl = function($scope, $rootScope, $timeout, $interval, $q, Bridge, FirebaseService, Storage, Directories, LocalAppsService, AccountService, hotkeys, AppMatchService, $firebaseObject, $firebaseArray) {
		
		$scope.platforms = {
		   "osx": {
	        "name": "macOS",
	        "reference": "osx",
	        "store": {
	            "app_link": "https://itunes.apple.com/app/id{id}",
	            "name": "Mac App Store"
	        },
	        "versions": {
						/*
						"arm": {
							"id": "Silicon",
							"name": "Apple Silicon",
							"reference": "arm",
							"release_year": "2020",
							"fullname": "Apple Silicon Macs",
							"type": "cpu",
							"_ui_statuspie_show": true
						},*/
						"macos26": {
							"features": [],
							"id": "26",
							"name": "Tahoe",
							"reference": "macos26",
							"release_year": "2025",
							"fullname": "macOS Tahoe",
							"_ui_statuspie_show": true
						},
						"macos15": {
							"features": [],
							"id": "15.0",
							"name": "Sequoia",
							"reference": "macos15",
							"release_year": "2024",
							"fullname": "macOS Sequoia",
							"_ui_statuspie_show": true
						},
						"macos14": {
							"features": [],
							"id": "14.0",
							"name": "Sonoma",
							"reference": "macos14",
							"release_year": "2023",
							"fullname": "macOS Sonoma",
							"_ui_statuspie_show": true
						},
						"ventura": {
							"features": [],
							"id": "13.0",
							"name": "Ventura",
							"reference": "ventura",
							"release_year": "2022",
							"fullname": "macOS Ventura",
							"_ui_statuspie_show": true
						},
						"monterey": {
							"features": [],
							"id": "12.0",
							"name": "Monterey",
							"reference": "monterey",
							"release_year": "2021",
							"fullname": "macOS Monterey",
							"_ui_statuspie_show": true
						},
						"big-sur": {
							"features": ["Apple Silicon"],
							"id": "11.0",
							"name": "Big Sur",
							"reference": "big-sur",
							"release_year": "2020",
							"fullname": "macOS Big Sur",
							"_ui_statuspie_show": true
						},
						"catalina": {
							"features": ["Catalyst"],
							"id": "10.15",
							"name": "Catalina",
							"reference": "catalina",
							"release_year": "2019",
							"fullname": "macOS Catalina",
							"_ui_statuspie_show": true
						},
	        	"mojave": {
	        		"features": ["Dark mode"],
	        		"id": "10.14",
	        		"name": "Mojave",
	        		"reference": "mojave",
	        		"release_year": "2018",
	        		"fullname": "macOS Mojave",
							"_ui_statuspie_show": true
	        	},
	        	"high-sierra": {
	        		"features": [],
	        		"id": "10.13",
	        		"name": "High Sierra",
	        		"reference": "high-sierra",
	        		"release_year": "2017",
	        		"fullname": "macOS High Sierra",
							"_ui_statuspie_show": true
	        	},
	        	"sierra": {
	        		"features": [],
	        		"id": "10.12",
	        		"name": "Sierra",
	        		"reference": "sierra",
	        		"release_year": "2016",
	        		"fullname": "macOS Sierra",
							"_ui_statuspie_show": true
	        	},
	        	"el-capitan": {
	        		"features": ["Metal", "Force Touch"],
	        		"id": "10.11",
	        		"name": "El Capitan",
	        		"reference": "el-capitan",
	        		"release_year": "2015",
	        		"fullname": "OS X El Capitan",
							"_ui_statuspie_show": false
	        	},
            "yosemite": {
            	"features": ["Handoff", "iCloud Drive", "Notification Centre widget"],
            	"id": "10.10",
            	"name": "Yosemite",
            	"reference": "yosemite",
            	"release_year": "2014",
            	"fullname": "OS X Yosemite",
							"_ui_statuspie_show": false
            },
            "mavericks": {
                "features": ["Maps", "App nap", "iCloud keychain", "Interactive notifications"],
                "id": "10.9",
                "name": "Mavericks",
                "reference": "mavericks",
                "release_year": "2013",
                "fullname": "OS X Mavericks",
								"_ui_statuspie_show": false
            },
            "mountain-lion": {
                "features": ["Gatekeeper", "Retina graphics", "Notification Center", "iCloud"],
                "id": "10.8",
                "name": "Mountain Lion",
                "reference": "mountain-lion",
                "release_year": "2012",
                "fullname": "OS X Mountain Lion",
								"_ui_statuspie_show": false
            },
            "lion": {
                "features": ["Versions", "Auto save", "Resume", "Fullscreen"],
                "id": "10.7",
                "name": "Lion",
                "reference": "lion",
                "release_year": "2011",
                "fullname": "OS X Lion",
								"_ui_statuspie_show": false
            },
            "snow-leopard": {
                "id": "10.6",
                "name": "Snow Leopard",
                "reference": "snow-leopard",
                "release_year": "2009",
                "features": ['64-bit', 'Grand Central Dispatch', 'OpenCL', 'Text substitution', 'Dictionary lookup'],
                "fullname": "OS X Snow Leopard",
								"_ui_statuspie_show": false
            }
	        }
	    }
		};
		
		$scope.state = {
			validBeta: true,
			isConnected: false,
			currentView: 'loading',
			windowFocus: 'focus',
			loadingMsg: '',
			systemInfo: {},
			search: '',
			filters: {
				apps: 'all',
				status: {
					os: 'macos14',
					status: 'All'
				},
				favourites: false,
				hideSystem: false,
				noMatch: false
			},
			viewAppDetail: null,
			lastFetched: null,
			lastFetchedStr: '',
			fetchInProgress: false
		};
		
		$scope.modal = {
			in: false,
			signin: false,
			preferences: false,
			experience: false,
			
			open: function(name) {
				$scope.modal.in = true;
				$scope.modal[name] = true;
			},
			
			close: function(name) {
				$scope.modal.in = false;
				$scope.modal[name] = false;
			}
		};
		
		$scope.settings = {
			matchIndicators: false
		};
		
		$scope.directories = [];
		$scope.apps = {
			local: null,
			matched: null,
			noMatch: []
		};
		
		$scope.appUpgrade = {
			dismissed: [],
			
			init: function() {
				if(Storage.has('ra:upgrade-dismissed')) {
					$scope.appUpgrade.dismissed = Storage.get('ra:upgrade-dismissed');
				}
			},
			
			dismiss: function() {
				if($scope.state.newerAppVersionAvailable) {
					$scope.appUpgrade.dismissed.push($scope.state.newerAppVersionAvailable);
					Storage.set('ra:upgrade-dismissed', $scope.appUpgrade.dismissed);
				}
			}
		};		
		//temporary for dev...
		$scope.nuke = function() {
			Storage.nuke();
		};
		
		$scope.refresh = function() {
			
			$scope.state.currentView = 'loading';
			
			$scope.fetchLocalApps();
		};
		
		$scope.makeUniqueID = function() {
			return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
			    var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
			    return v.toString(16);
			});
		};
		//get a list of locally installed apps from the bridge
		$scope.fetchLocalApps = function() {		
			$scope.state.currentView = 'loading';
			$scope.state.loadingMsg = 'Getting installed applications...';
						
			LocalAppsService.getApps().then(function(localApps) {				
				$timeout(function() {
					$scope.apps.local = localApps;
					
					console.log('Local apps', $scope.apps.local);
										
					//once we have the local apps, send off to get the remote apps
					$scope.getRemoteApps();
				});
			},
			//error getting local apps
			function(error) {
				if(error == 'timed-out') {
					console.log('LocalApps error received in controller.');
					$scope.state.currentView = 'local-error';
					$scope.state.loadingMsg = '';
				}
			});
		};
		
		$scope.debug = {
			showLogFile: function() {
				Bridge.send({messageType: 'showLogFile', messageData: true});		
			}
		};
		
		$scope.getRemoteApps = function() {
			if($scope.apps.local.length) {				
				$scope.state.loadingMsg = 'Checking compatibility...';
				$scope.state.fetchInProgress = true;
								
				AppMatchService.matchApps($scope.apps.local).then(function(matchedApps, error) {					
					$timeout(function() {
												
						if(error) {
							$scope.state.currentView = 'remote-error';
							$scope.state.fetchInProgress = false;
							$scope.state.loadingMsg = '';
						}
						else {
							
							console.log('matchedApps', matchedApps);
							
							$scope.state.currentView = 'apps-overview';
							$scope.apps.matched = matchedApps;
							
							$scope.processUnmatchedApps();
							
							//update sticky header
							$timeout(function() {
								$scope.stickyHeader.resizeListener();
							}, 1200);
							
							$scope.state.lastFetched = moment.unix(Storage.get('v2:lastFetch'));
							$scope.state.fetchInProgress = false;
							$scope.state.loadingMsg = '';
							
							//upload apps
							if($scope.account.preferencesForm.upload) {
								$scope.shareApps.upload();
							}
						}
					});
				});
			}
			else {
				console.log('no local apps...');
				$scope.state.currentView = 'local-error';
				$scope.state.fetchInProgress = false;
				$scope.state.loadingMsg = '';
			}
		};
		
		//find apps with no match
		$scope.processUnmatchedApps = function() {
			// Reset noMatch
			$scope.apps.noMatch = [];
		
			// Create a promise to handle async actions
			const promises = [];
		
			_.each($scope.apps.matched, function(app, index) {
				if (!app.matches || !app.matches.length) {
					const promise = $timeout(function() {
						$scope.apps.noMatch.push(app);
					});
					promises.push(promise);
				}
			});
		
			// Once all promises are resolved, continue with further logic
			$q.all(promises).then(function() {
				console.log('List of unmatched apps:', $scope.apps.noMatch);
				const appsJSON = JSON.stringify($scope.apps.noMatch);
				const messageString = 'List of unmatched apps: ' + appsJSON;
				Bridge.send({ messageType: 'log', messageData: messageString });
			});
		};

		
		/* Acount */
		$scope.account = {
			user: null,
			state: {
				view: '',
				loading: false,
				error: false,
				modalScreen: 1
			},
			init: function() {
				
				console.log('account init');
							
				//check to see if user data has been stored in local storage
				//will still check with firebase service for up-to-date user info
				var localUser = Storage.get('user');

				if(localUser) {
					$scope.account.user = localUser;
					
					console.log('localUser', localUser);
					
					//send to ObjC to switch account menu item enabled
					Bridge.send({messageType: 'signedIn', messageData: true});											
				}
				
				AccountService.on('user', function(user) {
					$timeout(function() {
						$scope.account.user = user;
																								
						if(user) {
							//send to ObjC to switch account menu item enabled
							Bridge.send({messageType: 'signedIn', messageData: true});
							
							//add listener for changes to user profile
							$scope.account.addProfileWatch(user.username);
							
							//save to local storage
							Storage.set('user', user);
							
							//get firebase preferences
							FirebaseService.rootConnection
							.child('user_profiles')
							.child(user.username)
							.child('visibility').on('value', function(snap) {
								$timeout(function() {
									if(snap.val()) {
										$scope.account.preferencesForm.visibility = snap.val();
									}
								});
							});
						}
						else {
							Bridge.send({messageType: 'signedOut', messageData: true});
							Storage.set('user', null);
						}
					});
				});
				
				//If the token is expired, show the login screen again
				AccountService.on('expired_token', function() {
					$timeout(function() {
						if(!$scope.account.user) $scope.modal.open('signin');
					});
				});
			},
			
			addProfileWatch: function(username) {
				FirebaseService.rootConnection.child('users').child(username).on('value', function(snap) {
					$timeout(function() {
						$scope.account.user = snap.val();
					});
				});
			},
			
			removeProfileWatch: function(username) {
				FirebaseService.rootConnection.child('users').child(username).off('value');
			},
			
			form: {
				email: '',
				password: ''
			},
			preferencesForm: {
				upload: false,
				visibility: 'private'
			},
			signin: function() {		
				$scope.account.state.error = false;		
				$scope.account.state.loading = true;
								
				AccountService.login($scope.account.form.email, $scope.account.form.password)
				.then(function(data) {

						$scope.account.state.loading = false;
						$scope.account.state.error = false;	
						$scope.account.state.modalScreen = 2;
						
						mixpanel.track('Sign in');
					
				})
				.catch(function(error) {
					$scope.account.state.loading = false;
					
					if(error.code == 'auth/wrong-password') {
						$scope.account.state.error = 'The email or password is incorrect.';
					}
					else {
						$scope.account.state.error = 'An error occurred signing in. Please try again.';
						
						mixpanel.track('Sign in error', {code: error.code});
					}	
				});
			},
			signout: function() {
				if(!$scope.account.user) return;
				
				var username = angular.copy($scope.account.user.username);
				AccountService.signout();
				$scope.account.user = null;
				$scope.account.removeProfileWatch(username);
				
				mixpanel.track('Sign out');
			},
			
			closeModal: function() {
				$scope.account.form.email = '';
				$scope.account.form.password = '';
				$scope.modal.close('signin');
				$scope.modal.close('preferences');
				$scope.account.state.modalScreen = 1;
			},
			
			savePreferences: function() {
				
				var vRef = window.FirebaseRoot.child('user_profiles');
				vRef.child($scope.account.user.username).child('visibility').set($scope.account.preferencesForm.visibility);
							
				Storage.set('account_option:upload_apps', $scope.account.preferencesForm.upload);
				Storage.set('account_option:visibility', $scope.account.preferencesForm.visibility);
				
				//process upload...
				if($scope.account.preferencesForm.upload) {
					$scope.shareApps.upload();
				}
			},
			
			visitProfilePage: function() {
				if($scope.account.user) $scope.openBrowser('https://roaringapps.com/u/' + $scope.account.user.username);
			},
			
			removeInstalledApps: function(macName) {
				
				if(!$scope.account.user) return;
				
				//if there's only one Mac in the list, go back to the main prefs screen
				if(Object.size($scope.account.user.installed) == 1) $scope.account.state.modalScreen = 1;
				
				var ref = FirebaseService.rootConnection
							.child('users').child($scope.account.user.username)
							.child('installed').child($scope.encodeReference(macName))
							.set(null);
				
				
			}
		};
				
		var init = function() {
		
			//0. Get or create UID
			if(Storage.has('uid')) UID = Storage.get('uid');
			else {
				UID = $scope.makeUniqueID();
				Storage.set('uid', UID);
			}
			
			//todo: for local testing, disable on release
			//mixpanel.disable();
			
			mixpanel.identify(UID);
						
			//1. check if this is a valid app version
			Bridge.on('systemInfo', function(data) {
				if(data.hasOwnProperty('raVersion')) {
				
					console.log('v', data.raVersion);
					console.log('systemInfo', data);
					
					SYSTEMINFO = data;
					
					$timeout(function() {
						$scope.state.systemInfo = data;
						
						console.log('osNumber', data.osNumber, data);
						
						//configure machine info
						if(data.osNumber.indexOf('10.9') > -1) {
								data.osName = 'Mavericks';
								data.osRef = 'mavericks';
						}
						else if(data.osNumber.indexOf('14.') === 0) {
								data.osName = 'Sonoma';
								data.osRef = 'macos14';
						}
						else if(data.osNumber.indexOf('13.') === 0) {
								data.osName = 'Ventura';
								data.osRef = 'ventura';
						}
						else if(data.osNumber.indexOf('12.') === 0) {
								data.osName = 'Monterey';
								data.osRef = 'monterey';
						}
						else if(data.osNumber.indexOf('11.') === 0) {
								data.osName = 'Big Sur';
								data.osRef = 'big-sur';
						}
						else if(data.osNumber.indexOf('10.15') > -1) {
								data.osName = 'Catalina';
								data.osRef = 'catalina';
						}
						else if(data.osNumber.indexOf('10.14') > -1) {
								data.osName = 'Mojave';
								data.osRef = 'mojave';
						}
						else if(data.osNumber.indexOf('10.13') > -1) {
								data.osName = 'High Sierra';
								data.osRef = 'high-sierra';
						}
						else if(data.osNumber.indexOf('10.12') > -1) {
								data.osName = 'Sierra';
								data.osRef = 'sierra';
						}
						else if(data.osNumber.indexOf('10.11') > -1) {
								data.osName = 'El Capitan';
								data.osRef = 'el-capitan';
						}
						else if(data.osNumber.indexOf('10.10') > -1) {
								data.osName = 'Yosemite';
								data.osRef = 'yosemite';
						}
						else if (data.osNumber.indexOf('10.8') > -1) {
								data.osName = 'Mountain Lion';
								data.osRef = 'mountain-lion';
						}
						else if (data.osNumber.indexOf('10.7') > -1) {
								data.osName = 'Lion';
								data.osRef = 'lion';
						}
						else if (data.osNumber.indexOf('10.6') > -1) {
								data.osName = 'Snow Leopard';
								data.osRef = 'snow-leopard';
						}
						
						$scope.state.systemInfo.osName = data.osName;
						$scope.state.systemInfo.osRef = data.osRef;
					});
					
					
					FirebaseService.checkValidBeta(data.raVersion, function(isValid, reason) {
						$timeout(function() {
							$scope.state.validBeta = isValid;
							$scope.state.validBetaReason = reason;							
						});
					});
					
					FirebaseService.checkForNewerAppVersion(data.raVersion, function(newVersion) {
						$timeout(function() {
							if(newVersion) $scope.state.newerAppVersionAvailable = newVersion;
							else $scope.state.newerAppVersionAvailable = false;
						});
					});
					
					//1b. log app has been opended
					FirebaseService.logEvent({type: 'app-open', os: data.osNumber, mac: data.macModel, appVersion: data.raVersion});
					
					//set mixpane super properties
					mixpanel.register({
						'ra-app-version': data.raVersion
					});
					
					//track app open
					mixpanel.track("Application open", {
						"os": data.osNumber,
						"mac": data.macModel
					});
				}
			});
	
			//1a. Setup listener for systemErrors (errors thrown by obj-c)
			Bridge.on('systemError', function(error) {
				console.log('Received systemError: ', error);
				var data = {
					type: 'systemError',
					subtype: error.type || 'unknown',
					exceptionName: error.name || null,
					exceptionReason: error.reason || null,
					exceptionUserInfo: error.userInfo || null
				};
				FirebaseService.logError(data);
			});
						
			//2. set directories
			$scope.directories = Directories.currentDirectories;
												
			//3. check if we have any info stored
			if(Storage.has('localApps') && Storage.has('matchedApps')) {			
								
				$scope.apps.local = Storage.get('localApps');
								
				if(Storage.has('v2:lastFetch')) {
					var lastFetch = moment.unix(Storage.get('v2:lastFetch')),
						now = moment();
					
					$scope.state.lastFetched = lastFetch;
											
					if(now.diff(lastFetch, 'minutes') > 120) {
						//get match in background
						$scope.getRemoteApps();
						
						//meanwhile, show out-of-date matchedapps
						$scope.apps.matched = Storage.get('matchedApps');
						$scope.processUnmatchedApps();
						$scope.state.currentView = 'apps-overview';
						
					}
					//else apps data is fresh
					else {
						$scope.apps.matched = Storage.get('matchedApps');
						$scope.processUnmatchedApps();
						$scope.state.currentView = 'apps-overview';
					}
				}
				else {
					//not sure when lastfetch was done, so re-get apps
					$scope.apps.matched = Storage.get('matchedApps');
					$scope.processUnmatchedApps();
					$scope.state.currentView = 'apps-overview';
				}
				
				//update sticky header once apps have loaded
				$timeout(function() {
					$scope.stickyHeader.resizeListener();
				}, 200);
				
				
			}
			//else if we don't have any local or remote apps,
			//show the landing screen
			else {
				//show start screen
				$scope.state.currentView = 'landing';
			}
						
			//wait for search text
			Bridge.on('search', function(data) {
				$timeout(function() {
					$scope.state.search = data;
				});
			});
			
			//when a new dir is added
			Bridge.on('chosenDirectoriesArray', function(data) {
				$timeout(function() {
					if(data) {
						Directories.add(data);
						$scope.directories = Directories.currentDirectories;
					}
				});
			});
			
			//window focus change
			Bridge.on('windowFocusStateChange', function(data) {
				$timeout(function() {
					if(data) $scope.state.windowFocus = data;
				});
			});
			
			//Open prefs window
			Bridge.on('menuItemPreferences', function(data) {
				$timeout(function() {
					$scope.modal.open('preferences');
				});
			});
			
			//sign in menu item
			Bridge.on('menuItemSignIn', function(data) {
				$timeout(function() {
					if(!$scope.account.user) $scope.modal.open('signin');
				});
			});
			
			//sign out menu item
			Bridge.on('menuItemSignOut', function(data) {
				$timeout(function() {
					$scope.account.signout();
				});
			});
			
			//keep the last fetched str updated every min
			$interval(function() {
				if($scope.state.lastFetched) $scope.state.lastFetchedStr = $scope.state.lastFetched.fromNow();
			}, 1000 * 60);
			//also update it when the lastFetched date changes
			$scope.$watch('state.lastFetched', function(d) {
				if(d) $scope.state.lastFetchedStr = $scope.state.lastFetched.fromNow();
				else $scope.state.lastFetchedStr = '';
			});
			
			//get existing account preferences
			if(Storage.has('account_option:upload_apps') && Storage.get('account_option:upload_apps') === true) {
				$scope.account.preferencesForm.upload = true;
			}
			if(Storage.has('account_option:visibility')) {
				$scope.account.preferencesForm.visibility = Storage.get('account_option:visibility');
			}
			
			//keyboard shortcuts
			hotkeys.add({
				combo: 'esc',
				description: 'Close the current modal window.',
				callback: function(event) {
					if($scope.modal.in) {
						event.preventDefault();
						$scope.modal.close('signin');
						$scope.modal.close('preferences');
						$scope.experience.closeModal();
					}
				}
			});
			
			$scope.appUpgrade.init();
			
			//firebase connection listener
			FirebaseService.connectionListener().then(null, null, function(isConnected) {
				$timeout(function() {
					$scope.state.isConnected = isConnected;
					$rootScope.$broadcast('connectionStatus', isConnected);
					console.log('FIREBASE CONNECTION: ', isConnected);
				});
			});
		};
		
		init();
		$scope.account.init();
				
		$scope.addDirectory = function() {
			Bridge.send({messageType: 'openDirectoriesModal', messageData: true});
		};
		$scope.removeDirectory = function(dir) {
			Directories.remove(dir);
			$scope.directories = Directories.currentDirectories;
		};
		
		/* App share/upload list */
		$scope.shareApps = {
			upload: function() {
								
				var option = Storage.get('account_option:upload_apps');
												
				if(!$scope.state.systemInfo) {
					console.log('no system info');
					return;
				}
				
				if(!$scope.account.user) {
					console.log('No user');
					return;
				}
								
				var data = {
					macName: $scope.state.systemInfo.macName || 'mac',
					macModel: $scope.state.systemInfo.macModel || 'unknown',
					osName: $scope.state.systemInfo.osName,
					osNumber: $scope.state.systemInfo.osNumber,
					lastFetch: $scope.state.lastFetched.unix(),
					share: Storage.get('account_option:visibility') || 'unknown',
					apps: {}
				}
			
				_.each($scope.apps.matched, function(app, index) {					
					if(!app.matches || !app.matches.length) return;
										
					var d = {
						bundle_id: app.bundleId,
						name: app.name,
						path: app.path,
						version: app.version,
						slug: app.matches[0].slug,
						_matchCertainty: app.matches[0]._matchCertainty
					};
					
					data.apps[d.slug] = d;
				});
								
				FirebaseService.rootConnection
				.child('users')
				.child($scope.account.user.username)
				.child('installed')
				.child($scope.encodeReference(data.macName))
				.set(data, function(error) {
				
				});
			}
		};
		
		
		/* Status parser fxn */
		$scope.statusParser = {
			compatibility: {
				platforms: {},
				appVersions: {},
				_appVersions: {}, //for sorted version selector
				notes: []
			},
			addPlatform: function(platform) {
				$scope.statusParser.compatibility.platforms[platform] = {};
				$scope.statusParser.compatibility.appVersions[platform] = {allVersions: {version: 'allVersions', name: 'All versions', type: 'Select'}};
			},
			addPlatformVersion: function(platform, platformVersion) {
								
				$scope.statusParser.compatibility.platforms[platform][platformVersion] = {allVersions: {
					total: 0,
					'Works fine': 0,
					"Doesn't work": 0,
					"Some problems": 0,
					result: undefined
				}};
			},
			addAppVersion: function(platform, platformVersion, appVersion) {
								
				$scope.statusParser.compatibility.platforms[platform][platformVersion][appVersion] = {
					total: 0,
					'Works fine': 0,
					"Doesn't work": 0,
					"Some problems": 0,
					result: undefined
				};
				if(!$scope.statusParser.compatibility.appVersions[platform].hasOwnProperty(appVersion)) {
					$scope.statusParser.compatibility.appVersions[platform][appVersion] = {version: appVersion, name: appVersion, type: 'App version'};
				}
			},
			
			//go through the status reports and work out the final result for the specified app version
			calculate: function(platform, platformVersion, appVersion) {
				var maxValue = 0, maxStatuses = [];
				
				//iterate over each report. If it has the biggest value, add the key ('report') to the maxStatuses array
				_.map($scope.statusParser.compatibility.platforms[platform][platformVersion][appVersion], function(val, key){
				    if(key == 'total' || key == 'result') return false;
				    
				    if(val !== 0 && val == maxValue) maxStatuses.push(key);
				    else if(val > maxValue) {
				    	maxValue = val;
				    	maxStatuses = [key];
				    }
				    
				});
				
				//if more than one 'report' has been added to the array, we have ourselves a draw
				if(maxStatuses.length > 1) $scope.statusParser.compatibility.platforms[platform][platformVersion][appVersion].result = 'Draw';
				//else we have a clear winner
				else {
					$scope.statusParser.compatibility.platforms[platform][platformVersion][appVersion].result = maxStatuses[0];
				}
				
				$scope.statusParser.compatibility.platforms[platform][platformVersion][appVersion].resultDashed = $scope.statusParser.compatibility.platforms[platform][platformVersion][appVersion].result.replace(' ', '-');
			}
		};
		
		//parse status report fxn (so can update display after sharing a status report)
		$scope.parseStatusReports = function(app, statusReports) {
						
			//reset
			$scope.statusParser.compatibility.platforms = {};
			$scope.statusParser.compatibility.appVersions = {}; 
			$scope.statusParser.compatibility._appVersions = {}; //for sorted version selector
			$scope.statusParser.compatibility.notes = [];
			
			console.log('Will parse status reports', app, statusReports);
			
			var loopPromises = [];
			
			//todo: check if app.matches exists...
			
			if(app.hasOwnProperty('platforms')) {
				//first, add each of the supported platforms
				//so that if there are no status reports for a particular platform
				//it will still be shown regardless
				angular.forEach(app.matches[0].platforms, function(platform) {
					$scope.statusParser.addPlatform(platform);
				});
			}
		
			//now iterate over each status report & extract the data
			angular.forEach(statusReports, function(report) {
				
				var deferred = $q.defer();
				loopPromises.push(deferred.promise);
				
				//prepare
				if(!$scope.statusParser.compatibility.platforms.hasOwnProperty(report.platform)) $scope.statusParser.addPlatform(report.platform);
				if(!$scope.statusParser.compatibility.platforms[report.platform].hasOwnProperty(report.platformVersion)) $scope.statusParser.addPlatformVersion(report.platform, report.platformVersion);
				if(!$scope.statusParser.compatibility.platforms[report.platform][report.platformVersion].hasOwnProperty(report.appVersion)) $scope.statusParser.addAppVersion(report.platform, report.platformVersion, report.appVersion);
				
				//add this report
				$scope.statusParser.compatibility.platforms[report.platform][report.platformVersion][report.appVersion][report.status] += 1;
				$scope.statusParser.compatibility.platforms[report.platform][report.platformVersion][report.appVersion].total += 1;
				
				$scope.statusParser.compatibility.platforms[report.platform][report.platformVersion]['allVersions'][report.status] += 1;
				$scope.statusParser.compatibility.platforms[report.platform][report.platformVersion]['allVersions'].total += 1;
				
				//recalculate the final status
				$scope.statusParser.calculate(report.platform, report.platformVersion, report.appVersion);
				$scope.statusParser.calculate(report.platform, report.platformVersion, 'allVersions');		
								
				//extract any notes		
				if(report.hasOwnProperty('note')) {
					
					$scope.statusParser.compatibility.notes.push(report);
				}
				
				deferred.resolve();
			});						
			
			//add to a separate array for version selector
			//note: not currently needed...
			/*
			angular.forEach($scope.statusParser.compatibility.appVersions, function(val, platform) {
				angular.forEach($scope.statusParser.compatibility.appVersions[platform], function(version) {
					if(!$scope.statusParser.compatibility._appVersions.hasOwnProperty(platform)) {
						$scope.statusParser.compatibility._appVersions[platform] = [];
					}
					$scope.statusParser.compatibility._appVersions[platform].push(version);
				});
			});
			*/
			
			$q.all(loopPromises).then(function () {
				console.log('foreach loop completed. Do something after it...');
				
				console.log('Finished parsing status reports', angular.copy($scope.statusParser.compatibility), $scope.apps.matched);
				
				var theApp = _.find($scope.apps.matched, function(o) {return o.id == app.id});
				
				console.log('Will update info for: ', theApp, $scope.apps.matched);
				
				$timeout(function() {
					theApp.matches[0].statuses = angular.copy($scope.statusParser.compatibility.platforms);
				});
				
				//todo: save to local storage the changes you've just made, so when the app is relaunched it is still correct. 
			});			
			
			//sort by app version
			/*
			angular.forEach($scope.statusParser.compatibility._appVersions, function(val, platform) {
				val.sort(function(a,b) {
					//if the version is just a single number (e.g. "5"), change that to "5.0" so it sorts properly
					if(!isNaN(a.version) && a.version.indexOf('.') === -1) a.version = a.version + '.0';
					if(!isNaN(b.version) && b.version.indexOf('.') === -1) b.version = b.version + '.0';
					
					return VersionCompare.compare(a.version, b.version);
				});
			});
			*/
								
		};
		
		/* Share experience */
		$scope.experience = {
			state: {
				loading: false,
				error: null
			},
			currentApp: undefined,
			currentAppIndex: undefined,
			newStatusReport: {
				platform: 'osx',
				platformVersion: undefined,
				appVersion: undefined,
				status: undefined,
				note: null
			},
			
			start: function(app, appIndex) {
				$scope.experience.currentApp = app;
				$scope.experience.currentAppIndex = appIndex;
				
				$scope.experience.newStatusReport.appVersion = app.version;
				$scope.experience.newStatusReport.platformVersion = $scope.state.systemInfo.osRef;
				$scope.modal.open('experience');
			},
			summarySentence: function() {
				var s = $scope.experience.newStatusReport,
					c = $scope.platforms,
					statuses = {'works-fine': 'works fine', 'some-problems': 'has some problems', 'doesnt-work': 'doesn\'t work'}
				if (s.platform && s.platformVersion && s.appVersion && s.status) {
					return $scope.experience.currentApp.name + ' version ' + s.appVersion + ' ' + statuses[s.status] + ' on ' + c[s.platform].versions[s.platformVersion].fullname + '.';
				}
				else return null;
			},
			invalid: function() {
				
				var invalid = false;
				
				if($scope.experience.newStatusReport.appVersion == undefined || $scope.experience.newStatusReport.status == undefined) {
						invalid = true;
					}
				else {
					invalid = false;
				}
				
				return invalid;
			},
			submit: function() {	
				var statuses = {'works-fine': 'Works fine', 'some-problems': 'Some problems', 'doesnt-work': 'Doesn\'t work'},
						cantProceed = false;
				
				/* Validations */
				//0. Check internet connection...
				if(!$scope.state.isConnected) {
					console.log('Cannot submit status report - no Firebase connection.');
					$scope.experience.state.error = 'Connect to the internet to share compatibility information.';
					cantProceed = true;
				}
				
				//1. Check the selected app has a match in the RA database
				var matchedApp = $scope.experience.currentApp.matches[0];
				if(!matchedApp) {
					console.log('Cannot submit status report for unmatched app');
					$scope.experience.state.error = 'Could not match app in RoaringApps database. Cannot share compatibility status.';
					cantProceed = true;
				}
					
				//check if signed in
				if(!$scope.account.user) {
					console.log('Not signed in, can\'t proceed.');
					$scope.experience.state.error = 'Sign in to your RoaringApps account to share compatibility information.';
					cantProceed = true;
				}
				
				if(!cantProceed) {
					$scope.experience.state.loading = true;
					
					var slug = matchedApp.slug;
									
					console.log('ca', $scope.experience.currentApp);
					
					if(!$scope.experience.newStatusReport.appVersion) {
						$scope.experience.newStatusReport.appVersion = 'unknown'; 
					}
					
					//get the data ready
					$scope.experience.newStatusReport.status = statuses[$scope.experience.newStatusReport.status];
					$scope.experience.newStatusReport.user = $scope.account.user.username || 'anonymous';
					$scope.experience.newStatusReport.$priority = moment().unix();
					$scope.experience.newStatusReport.via = 'macapp:'+$scope.state.systemInfo.raVersion;
					
					//get existing status reports for this app & the current user
					var allStatusReports = $firebaseArray(window.FirebaseRoot.child('v3/apps_meta/' + slug + '/status_reports'));
					var userStatusReportsRef = window.FirebaseRoot.child('v3/status_reports_by_user');
					userStatusReportsRef = userStatusReportsRef.child($scope.account.user.username).child(slug);
					var userStatusReports = $firebaseArray(userStatusReportsRef);
					
					allStatusReports.$loaded()
					  .then(function(x) {
					    console.log('asr loaded', x);
							
							//continue with whatever you're doing
							//check to see if this user has already create a report for this platform/platformVersion/appVersion combo
							var existingReport = _.find(allStatusReports, function(r) {
								return r.platform == $scope.experience.newStatusReport.platform
									&& r.platformVersion == $scope.experience.newStatusReport.platformVersion
									&& r.appVersion == $scope.experience.newStatusReport.appVersion
									&& r.user == $scope.experience.newStatusReport.user
							});
							
							//if a report already exists, just change the status & then save it
							if(existingReport) {
								
								console.log('Existing report, will adjust', existingReport);
								
								existingReport.status = $scope.experience.newStatusReport.status;
								existingReport.note = $scope.experience.newStatusReport.note || null;
								existingReport.$priority = moment().unix();
								
								allStatusReports.$save(existingReport);
								
								userStatusReports.$loaded()
								.then(function() {
									console.log('user status reports loaded');
									
									//and we also need to update the 'status_reports_by_user' record as well
									var userReport = _.find(userStatusReports, function(r) {
										return r.platform == $scope.experience.newStatusReport.platform
											&& r.platformVersion == $scope.experience.newStatusReport.platformVersion
											&& r.appVersion == $scope.experience.newStatusReport.appVersion
											&& r.user == $scope.experience.newStatusReport.user
									});
									
									//just in case something has gone wrong and there isn't an existing user report
									//create it from scratch
									if(!userReport) {
										userStatusReports.$add($scope.experience.newStatusReport).then(function() {
											$rootScope.$broadcast('statusReportSaved', true);
										});
									}
									//else update the existing one
									else {
										userReport.status = $scope.experience.newStatusReport.status;
										userReport.note = $scope.experience.newStatusReport.note || null;
										userReport.$priority = moment().unix();
										
										userStatusReports.$save(userReport).then(function() {
											$rootScope.$broadcast('statusReportSaved', true);
										});
									}
									
									
								});
							}
							//else save a new report
							else {
								
								console.log('Will save new report');
								
								//send the status report to Firebase
								allStatusReports.$add($scope.experience.newStatusReport).then(function() {
									console.log('Saved sr');
									$rootScope.$broadcast('statusReportSaved', true);
									
								});
								userStatusReports.$add($scope.experience.newStatusReport);	
							}
							
							
							var deregisterListener = $scope.$on('statusReportSaved', function (event, data) {
								console.log('inside $once statusReportSaved');
								
								var sr = angular.copy($scope.experience.newStatusReport);
								delete sr['$priority'];
								
								//create server task
								FirebaseService.createServerTask('app_new_status', {slug: slug, statusReport: sr});
								FirebaseService.logEvent({type: 'status-share', slug: slug, raAppVersion: $scope.state.systemInfo.raVersion || 'unknown'});
								mixpanel.track('Status share', {
									'app': slug	
								});
								
								//send statusReports to statusParser so we can update the table info for this app
								console.log('all status reports', angular.copy(allStatusReports));
								//todo: find the app $index
								$scope.parseStatusReports($scope.experience.currentApp, allStatusReports);
																		
								//we're done - reset data & close the panel
								$scope.experience.closeModal();
								$scope.experience.state.loading = false;
																
								//we only want to listen for this event once, so by calling the saved var it is deregistered.
								deregisterListener();
							});							
					  })
						.catch(function(error) {
					    console.log("Error loading allStatusReports:", error);
					  });
				}
			},
			closeModal: function() {
				$scope.modal.close('experience');
				$timeout(function(){
					$scope.experience.reset();
				},300);
			},
			reset: function() {
				$scope.experience.state.loading = false;
				$scope.experience.state.error = null;
				$scope.experience.currentApp = undefined;
				$scope.experience.currentAppIndex = undefined;
				$scope.experience.newStatusReport = {
					platform: 'osx',
					platformVersion: undefined,
					appVersion: undefined,
					status: undefined,
					note: null
				};
			}
			
		};
		
		//helper fx
		//src: http://adripofjavascript.com/blog/drips/making-deep-property-access-safe-in-javascript.html
		//obj = {}, props = [], defaultValue = anything
		var deepGet = function(obj, props, defaultValue) {
		    if (obj === undefined || obj === null) {
		        return defaultValue;
		    }
		
		    if (props.length === 0) {
		        return obj;
		    }
		    var foundSoFar = obj[props[0]];
		    var remainingProps = props.slice(1);
		
		    return deepGet(foundSoFar, remainingProps, defaultValue);
		};
		
		$scope.getStatus = function(statuses, osVersion) {
			var status = deepGet(statuses, [osVersion, 'allVersions'], null);
			if(status !== null) {
				return 'status-' + status.result.toLowerCase().replace(/[\s\']/g, '');
			}
			else {
				return 'status-nodata';
			}
		};
		
		$scope.openAppUrl = function(app) {
		
			if(app.hasOwnProperty('matches') && app.matches.length && app.matches[0].hasOwnProperty('slug')) {
				$scope.openBrowser('https://roaringapps.com/app/' + app.matches[0].slug);
			}
		};
		
		$scope.openBrowser = function(url) {
			Bridge.send({messageType: 'openURL', messageData: url});
		};
		$scope.ago = function(str) {
			return moment.unix(str).fromNow() || '';
		};
		Object.size=function(obj){var key,size=0;for(key in obj)obj.hasOwnProperty(key)&&size++;return size};
		$scope.objectSize = function(obj) {
			return Object.size(obj);
		};
		$scope.encodeReference = function(t) {
			return t.replace(/\./g,",").replace(/[$\[\]#\/]/g,"")
		};
		
		/* controls the sticky header stuff */
		$scope.stickyHeader = {
			
			theStickyHeader: undefined,
			columns: undefined,
			scrollContainer: undefined,
			
			_init: function() {
				window.addEventListener('resize', this.resizeListener);
				this.scrollListener();
				
			},
			
			//called on page load to setup the scroll listeners
			//and clone the table thead
			init: function() {			
				this.theStickyHeader = $('.apps-list-2016 .header');
				this.columns = $('.apps-list-2016 .body .row:visible:first-child .table-col');
				this.scrollContainer = $('.apps-list-2016');
			},
						
			scrollListener: function(ev) {
				if(!$scope.stickyHeader.scrollContainer) $scope.stickyHeader.init();
								
				$scope.stickyHeader.scrollContainer.on('scroll', function(ev) {
					$scope.stickyHeader.theStickyHeader.css('transform', 'translateX(-' + $scope.stickyHeader.scrollContainer.scrollLeft() + 'px)');
				});
				
			},
			
			resizeListener: function() {				
				if(!$scope.stickyHeader.columns) $scope.stickyHeader.init();
				
				$scope.stickyHeader.theStickyHeader.width($scope.stickyHeader.scrollContainer[0].scrollWidth + 4);
				$scope.stickyHeader.columns.each(function(index, el) {
					var index2 = index + 1;
					$('.sticky-col:nth-child('+index2+')').width($(el).width());
				});
			}
		};	
		
		$scope.stickyHeader._init();
		
		$scope.$watch('apps.matched', function() {
			$timeout(function() {
				$scope.stickyHeader.init();
			}, 1000);
		});
		
		//any time an app filter is changed, reset the stickyheader columns
		//so that they still grab a visible column to size to
		$scope.$watch('state.filters', function(filters) {
			$timeout(function() {
				$scope.stickyHeader.init();
			}, 100);
		}, true);
		$scope.$watch('state.search', function(filters) {
			$timeout(function() {
				$scope.stickyHeader.init();
			}, 100);
		}, true);
		
	}; //MainCtrl()
	
	RoaringApps.controller('MainCtrl', ['$scope', '$rootScope', '$timeout', '$interval', '$q', 'Bridge', 'FirebaseService', 'Storage', 'Directories', 'LocalAppsService', 'AccountService', 'hotkeys', 'AppMatchService', '$firebaseObject', '$firebaseArray', MainCtrl]);
})();
