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

var CallLogList = (function() {
	
	// maximum number of entries to store in cache without visiting the active call scene
	const kCallLogMax = 100;
	
	// number of call log entries to grab at once
	const kQueryBlockSize = 100;
	
	// placeholder numbers we can receive from the carrier
	const kUnknownNumbers = ["","unknown","unknown caller","blocked","blocked caller"];
	
	// cache and cache flags
	var cache, zeroOffset, cacheInvalidatedAt, cacheInvalidatedAtTZ;
	
	// store active service requests that aren't tied to a SceneController
	var activeServiceRequests = $H(), activeServiceRequestsUID = 0;
	
	// cache has been modified since hasChanged() last called
	var hasChanged = false;
	
	// resets the internal cache
	function invalidateCache() {
		cache = {};
		zeroOffset = new Date(0, 0, 0, 0, 0, 0, 0).getTime();
		
		var now = new Date();
		cacheInvalidatedAt = now;
		cacheInvalidatedAtTZ = now.getTimezoneOffset();
	}
	
	// set up cache now
	invalidateCache();
	
	// adds basic formatting to a calllog item
	function formatItem( item ) {
		// add duration
		if (item.type === "missed" || Mojo.Controller.getAppController().assistant.hideCallLogCallDuration === true || item.duration < 1000) { 
			item.durationDisplayText = "";
		} else {
			var duration = new Date(parseInt(item.duration,10) + zeroOffset);
			item.durationDisplayText = DateFormatter.formatDuration(duration);
		}
		
		// add timeseparator and start time
		var startTime = new Date(parseInt(item.startTime,10));
		item.timeSeparator = Mojo.Format.formatRelativeDate(startTime, {date:'short'});
		item.startTimeDisplayText = Mojo.Format.formatDate(startTime, {time:'short'});
		
		// No number received from carrier
		if ( kUnknownNumbers.include(item.number) ) {
			item.displayNumber = '';
			item.displayLabel = '';
			item.displayShortlabel = $L('Unknown');
			
		// Number received from carrier
		} else {
			item.displayNumber = FormatPhoneNumber(item.number);
			
			// Wasn't in our contacts
			var label = item.label;
			if (label == -1) {
				item.displayLabel = '';
				item.displayShortlabel = '';
				
			// Was in our contacts
			} else {
				item.displayLabel = Messages.phoneLabel[label] || label;
				item.displayShortlabel = Messages.phoneLabelShort[label] || '';
			}
		}
		
		// delete unused attributes
		delete item.duration;
		delete item.startTime;
		
		return item;
	}
	
	// applies formatting to a calllog subcall
	function formatSubCall( item ) {
		// add parenthesis to duration
		item.durationDisplayText = (item.durationDisplayText ? "(" + item.durationDisplayText + ")" : "");
		
		// delete unused attributes
		delete item.contactId;
		delete item.displayName;
		delete item.number;
		delete item.pictureLoc;
		delete item.label;
		delete item.timeSeparator;
		delete item.displayLabel;
		delete item.subTitle;
		
		return item;
	}
	
	// applies formatting to a call log group header
	function formatGroupHeader( item ) {
		
		// first subcall is always a repeat of the group header
		var firstcall = Object.clone(item);
		formatSubCall(firstcall);
		item.calls = [firstcall];
		
		// format picture
		if ( item.contactId == -1 || item.pictureLoc === "" || item.pictureLoc === "no" ) {
			item.pictureLoc = "images/list-avatar-default.png";
		}
		
		// undialable numbers get the disabled
		if (kUnknownNumbers.include(item.number)) {
			item.disabled = "disabled";
		}
		
		if ( item.displayLabel && ! item.displayLabel.blank() ) {
			item.subTitle = item.displayLabel + ", " + item.startTimeDisplayText;
		} else {
			item.subTitle = item.startTimeDisplayText;
		}
		
		// delete unused attributes
		delete item.startTimeDisplayText;
		delete item.displayShortlabel;
		delete item.durationDisplayText;
		
		return item;
	}
	
	// comparator for determining if two items should be merged
	function itemComparator(a,b) {
		
		// must be defined
		if ( a === undefined || b === undefined ) {
			return false;
		}
		
		// must always be same contact on same day
		if ( a.contactId !== b.contactId || a.timeSeparator !== b.timeSeparator ) {
			return false;
		}
		
		// if unknown contact then the numbers must match and can't be 'unknown'
		if ( a.contactId == -1 && a.number !== b.number) {
			return false;
		}
		
		return true;
	}
	
	// coalescence <newitems> and add them to <cache>
	function dedup(newitems, cache) {
		//Mojo.Log.info("dedup at cache_offset %j, cache.length %j", cache.length)
		for (var i=0, j=0, prev = cache.last(), length = newitems.length; i < length; i++) {
			var item = formatItem(newitems[i]);
			
			//Mojo.Log.info("---------- %j (name: %j) (prev: %j)",item, item.contactId, (prev && prev.contactId));
			
			// CASE: similar consecutive items
			if ( itemComparator(prev, item) ) {
				//Mojo.Log.info("same as before! adding to prev's subitems");
				formatSubCall(item);
				
				prev.calls.push(item);
				
			// CASE: different than prev, reset prev
			} else {
				//Mojo.Log.info("NOT the same as before. resetting prev. adding to end");
				formatGroupHeader(item);
				cache.push(item);
				prev = item;
				j++;
			}
		}
		return (i - j); // number merged
	}
	
	// PUBLIC
	return {
		// called externally to check if cache is cache has been changed since last call
		// also checks if cache needs to be updated because of a change in day or timezone
		hasChanged: function() {
			var date = new Date();
			var then = Mojo.Format.formatRelativeDate(cacheInvalidatedAt, { date: 'short' });
			var now = Mojo.Format.formatRelativeDate(date, { date: 'short' });
			// new day or new timezone
			if (then !== now || cacheInvalidatedAtTZ !== date.getTimezoneOffset()) {
				invalidateCache();
				hasChanged = true;
			}
			var response = hasChanged;
			hasChanged = false;
			return response;
		},
		
		addEntry: function(callId, number, type, startTime, duration, contactEntry){
						
			var appAssistant = Mojo.Controller.getAppController().assistant;
			var contacts = appAssistant.contacts;
	       	var picLoc = undefined;
			var label = -1;
			
			// don't log numbers with otasp prefix
			if (PlatformType.isOtaspNumber(number)) {
				return;
			}
			
			// don't log emergency numbers (if preference set)
			if (appAssistant.hideEmergencyNumbersFromCallLog && PlatformType.isEmergencyNumber(number))  {
				appAssistant.contacts.lastEmergencyNumber = number;
				return;
			}
			
			var displayName = "", contactId = '-1';
			if (contactEntry === undefined || contactEntry.id === undefined) {
				// get entry from carrier list if possible
				var carrierBookEntry = contacts.carrierBook[number];
				
				if (carrierBookEntry) {
					QDLogger.log( "addEntry", "got carrierbook entry: " , carrierBookEntry);
					displayName = $L(carrierBookEntry || '');
					picLoc = "";
				} else {
					QDLogger.log( "addEntry", "no carrierbook entry");
					displayName = FormatPhoneNumber(number);
					picLoc = "no";
				}
				
			} else {
				if (contactEntry.label !== undefined) {
					label = contactEntry.label;
				}
				displayName = contactEntry.name || FormatPhoneNumber(number);
				contactId = contactEntry.id;
				picLoc = contactEntry.pictureLocSmall;
			}
			
			if (type === "outgoing") {
				//Save the dialed number and other info locally
				appAssistant.contacts.saveLastDialedNumber(number,contactId, displayName, label);
			}
			
			var params = {
				"contactId": contactId,
				"displayName": displayName,
				"number": number,
				"type": type,
				"startTime": startTime,
				"duration": duration,
				"pictureLoc": picLoc,
				"label": label
			};
			
			Mojo.Log.info("addEntry: %j",params);
			// ex: com.palm.superlog	//addEntry	«string=“{"contactId": "31885837205782", "displayName": "Stacey Webster", "number": "7034729133", "type": "outgoing", "startTime": 1262821293537, "duration": 13290, "pictureLoc": "/var/luna/files/STACEYWEBSTER1262808312019.jpg", "label": "Work"}”, string=“com.palm.app.phone 1005”»
			
			var uid = activeServiceRequestsUID++;
			var request = new Mojo.Service.Request("palm://com.palm.superlog", {
				method: 'addEntry',
				parameters: params,
				onSuccess: this.onAddEntrySuccess.bind(this, params),
				onFailure: function() {
					Mojo.Log.error( "CallLogList#addEntry", "unable to write call log entry");
				},
				onComplete: function(uid) {
					activeServiceRequests.unset(uid);
				}.curry(uid)
			});
			activeServiceRequests.set(uid, request);
		},
		
		onAddEntrySuccess: function(item, response) {
			// OPTIMIZATION: handle common case of adding one entry at the beginning of the cache
			
			// helper function adds an item to front of cache, merging with first item if necessary
			var unshiftIntoCache = function (item, cache) {
				
				// clone to make sure we're not aliasing since this
				// function can be called again with the same item
				item = Object.clone(item);
				formatItem(item);
				formatGroupHeader(item);
				
				// if first item in cache is similar, absorb it and its subcalls
				if ( cache.list.length > 0 && itemComparator(cache.list[0], item) ) {
					// remove top of cache, retrieve its subcalls, and merge into item
					var othercalls = cache.list.shift().calls;
					item.calls = item.calls.concat(othercalls);
					
				// else, make sure we let the list know we have one more
				} else {
					cache.calculatedtotal++;
				}
				cache.list.unshift(item);
				cache.readoffset++;
				cache.total++;
				hasChanged = true;
				
				// WORKAROUND for v1 list bug
				// Note: this check should really be cache.total > kCallLogMax,
				// however, if the last item in the cache we're removing is in the list widget's
				// initial render window (currently 0 - 95), it will show trigger blank rows
				// caused by splitInitialRender parameter
				if ( cache.list.length > kCallLogMax ) {
					this.removeLastFromCache(cache);
				}
			}.bind(this);
			
			item.id = response.id;
			
			if ( cache[''] ) {
				unshiftIntoCache(item, cache['']);
			}
			
			if ( cache['missed'] && item.type == "missed" ) {
				unshiftIntoCache(item, cache['missed']);
			}
			
			// wait for scene transition, if any
			(function() {
				var app = Mojo.Controller.getAppController();
				var sc = app.getStageController("PhoneApp");
				if ( sc && app.assistant.topScene() == "calllog" ) {
					sc.delegateToSceneAssistant('maybeRefreshLists');
				}
			}).defer();
		},
		
		// helper function removes the last entry from the cache. for limiting the cache size when adding calls.
		removeLastFromCache: function(cache) {
			//Mojo.Log.info("before popping off cache calculatedtotal: %s, readoffset: %s, cache length: %s", cache.calculatedtotal, cache.readoffset, cache.list.length)
			var lastelement = cache.list.last();
			// CASE: last has subcall, pop that off
			if ( lastelement.calls.length > 1 ) {
				lastelement.calls.pop();
				cache.calculatedtotal++;
			// CASE: last has no subcalls other its own, pop it off
			} else {
				cache.list.pop();
			}
			cache.readoffset--;
			//Mojo.Log.info("after popping off cache calculatedtotal: %s, readoffset: %s, cache length: %s", cache.calculatedtotal, cache.readoffset, cache.list.length)			
		},
		
		deleteEntries: function(ids, type, nowcallback, aftercallback, scenecontroller) {
			var itemId = ids[0]; // the first id will always be the id of the item we're looking for
			type = type || '';
			var cache_ref = cache[type];
			
			for (var i=0, l=cache_ref.list.length; i < l; i++) {
				// found the element
				if ( cache_ref.list[i].id == itemId ) {
					var cleanup;
					
					// remove the element
					cache_ref.list.splice(i,1);
					cache_ref.calculatedtotal--;
					
					// two consecutive entries created, create a cleanup function to merge them
					var previous = cache_ref.list[i-1], next = cache_ref.list[i];
					if ( itemComparator(previous, next) ) {
						
						cleanup = function(i,cache_ref,cb) {
							previous.calls = previous.calls.concat(next.calls);
							cache_ref.list.splice(i,1);
							cache_ref.calculatedtotal--;
							return previous;
						}.bind(this,i,cache_ref);
					}
					
					nowcallback(cleanup);
					break;
				}
			}
			
			// always invalidate the cache that isn't this type
			// it will get updated in the background
			if ( type == "missed" ) {
				delete cache[''];
			} else {
				delete cache['missed'];
			}
			
			var reqcount = ids.length;
			ids.each(function(id) {
				scenecontroller.serviceRequest("palm://com.palm.superlog", {
					method: 'deleteEntry',
					parameters: { id: id },
					onSuccess: function() {
						cache[type].readoffset--;
						cache[type].total--;
					},
					onFailure: function(e) {
						Mojo.Log.error("CallLogList#deleteEntries failure: %j",e);
					},
					onComplete: function() {
						reqcount--;
						if ( reqcount === 0 ) {
							aftercallback();
						}
					}.bind(this)
			    });
			}, this);
		},
		
		deleteAll: function(callback, scenecontroller) {
			// OPTIMIZATION: reset caches and call callback now
			var resetCache = function(cache) {
				cache.list = [];
				cache.calculatedtotal = 0;
				cache.readoffset = 0;
				cache.total = 0;
			};
			if ( cache[''] ) {
				resetCache(cache['']);
			}
			if ( cache['missed'] ) {
				resetCache(cache['missed']);
			}
			callback();
			
			scenecontroller.serviceRequest("palm://com.palm.superlog", {
				method: 'deleteAll',
				onFailure: function(e) {
					Mojo.Log.error("CallLogList#deleteAll failure: %j",e);
				}
		    });
		},
		
		logCount: function(type, callback) {
			type = type || "";
			
			var cache_ref = cache[type];
			if ( cache_ref === undefined ) {
				this.logCountFromService(type, function(count) {
					cache[type] = {
						readoffset: 0, // offset where the next query will start
						calculatedtotal: count, // what we think is the index of the last item in the list
												// this value is adjusted each time we merge items together
						total: count, // total number of items in database table. doesn't change
						list: [] // the cache
					};
					callback(count);
				});
			} else {
				callback(cache_ref.calculatedtotal);
			}
		},
		
		logCountFromService: function(type, callback) {
			var uid = activeServiceRequestsUID++;
			var request = new Mojo.Service.Request("palm://com.palm.superlog", {
				method: 'logCount',
				parameters: {"type": type},
				onSuccess: function(payload) {
					if ( payload.count === undefined ) {
						Mojo.Log.error("Unable to query count for type %s, payload: %s",type,payload);
					}
					callback(payload.count);
				},
				onFailure: function(e) {
					Mojo.Log.error("CallLogList#logCount failed with %j",e);
				},
				onComplete: function(uid) {
					activeServiceRequests.unset(uid);
				}.curry(uid)
			});
			activeServiceRequests.set(uid, request);
		},
		
		logQuery: function(offset, count, type, callback, setlengthcallback) {
			type = type || "";
			callback = callback || Mojo.doNothing;
			setlengthcallback = setlengthcallback || Mojo.doNothing;
			
			// call logCount first to make sure cache intialized and calculatedtotal set
			this.logCount(type, this.logQueryHelper.bind(this, offset, count, type, callback, setlengthcallback));
		},
		
		logQueryHelper: function(offset, count, type, callback, setlengthcallback) {
			var cache_ref = cache[type], offset_end = (offset + count);
			
			Mojo.Log.info('Asking for type %j offset %j count %j, cache length: %s, calctotal: %s, total: %s, readoffset: %s',type,offset,count,cache_ref.list.length,cache_ref.calculatedtotal,cache_ref.total,cache_ref.readoffset);
			
			// CASE: have enough, or what we have is all we can get
			if ( offset_end <= cache_ref.list.length || cache_ref.calculatedtotal == cache_ref.list.length ) {
				//Mojo.Log.info('have enough (cache has %j), returning slice(%j,%j), calctotal: ',cache_ref.list.length,offset,(offset + count),cache_ref.calculatedtotal);
				this.logQueryDispatchCallback(cache_ref, offset, count, callback, setlengthcallback);
			}
			// CASE: need more, query from service
			else {
				this.logQueryFromService(offset, count, type, callback, setlengthcallback, cache_ref);
			}
		},
		
		logQueryFromService: function(offset, count, type, callback, setlengthcallback, cache_ref) {
			var howmany = Math.min(kQueryBlockSize, cache_ref.total - cache_ref.readoffset);
			var params = {
				offset: cache_ref.readoffset,
				limit: howmany,
				type: type
			};
			//Mojo.Log.info("logQueryFromService: params: %j",params);
			cache_ref.readoffset += howmany;
			
			//Mojo.Log.info('querying at readoffset ',cache_ref.readoffset);
			var uid = activeServiceRequestsUID++;
			var request = new Mojo.Service.Request("palm://com.palm.superlog", {
				method: 'logQuery',
				parameters: params,
				onSuccess: function(type, offset, count, callback, setlengthcallback, payload) {
					var cache_ref = cache[type];
					var offset_end, nummerged;
					
					// cache[type] could have been invalidated while waiting for this call to return
					// in that case, ignore this response entirely
					if ( cache_ref && payload.list ) {
						offset_end = (offset + count);
						nummerged = dedup(payload.list, cache_ref.list);
						
						cache_ref.calculatedtotal -= nummerged;
						
						Mojo.Log.info("number merged: %s, new caclulatedtotal: %s, cache length: %s", nummerged, cache_ref.calculatedtotal, cache_ref.list.length);
						
						// didn't satisfy request and there may be more, recursively load next batch
						if (cache_ref.list.length < offset_end && payload.list.length == params.limit) {							
							this.logQueryHelper(offset, count, type, callback, setlengthcallback);
							
						// request satisfied or no more to query, return
						} else {
							// return the previous one also as it may have had subcalls added to it
							if ( offset !== 0 ) {
								offset--;
								count++;
							}
							this.logQueryDispatchCallback(cache_ref, offset, count, callback, setlengthcallback);
						}
					}
				}.bind(this, type, offset, count, callback, setlengthcallback),
				onFailure: function() {
					QDLogger.error("logQuery failure");
				},
				onComplete: function(uid) {
					activeServiceRequests.unset(uid);
				}.curry(uid)
		    });
			activeServiceRequests.set(uid, request);
		},
		
		logQueryDispatchCallback: function(cache_ref, offset, count, callback, setlengthcallback) {
			// WORKAROUND: the list doesn't like it when you give it an empty array of items
			if ( offset < cache_ref.list.length ) {
				callback(offset, cache_ref.list.slice(offset, offset + count));
			}
			setlengthcallback(cache_ref.calculatedtotal);
		}
	};
})();

CallLogList.typeLookupTable = $H({
    incoming: Messages.logIncoming,
    missed: Messages.logMissed,
    outgoing: Messages.logOutgoing
});

