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


//a call object is created whenever the TS sends up a new macro-notif
//and it doesn't match the current TelephonyCallState info.

//the macro-notif is "parsed" into a TelephonyCallState object 

var TelephonyCallState = Class.create({
	conferenceId: -1,
	
	// takes a serviceMessage containing a macroCall event
	// optionally takes the previous state to pull starttimes, disconnected calls, 
	// and contact info from
	initialize: function(serviceMessage, oldState, dialParams) {
        Mojo.Log.info( "PhoneApp: TelephonyCallState::initialize %j", serviceMessage);
		
		// TODO FIXME: this will do for now.
		this.appAssistant = Mojo.Controller.getAppController().assistant;
		this.contacts = this.appAssistant.contacts; 
		
		if (oldState) {
			this.lines = oldState.lines;
			this.disconnectDialPendingCall = oldState.disconnectDialPendingCall;
		} else {
			this.lines = [];
			this.disconnectDialPendingCall = false;
		}
		
		// fake in a dialing call
		if (dialParams !== undefined) {
			this.conjureDialingMessage(dialParams);
		// or actually process a real message
		} else {
		// TODO: refactor this into something that grabs params
		// and something that normalizes
			if (serviceMessage.macroEventCalls) {
				this.action = serviceMessage.macroEventCalls.event;
			}
			if (serviceMessage.macroEventCalls != undefined && serviceMessage.macroEventCalls.eventCall != undefined) {
				var event = serviceMessage.macroEventCalls.eventCall.eventCalls;		
	
				this.previousAction = event.previousStatus;
			
				if (event.call !== undefined) {
					this.affectedCallId = event.call.id;
					this.cnapName = 
						(event.call.cnapName == "unknown" ? "" : event.call.cnapName);
					this.number = event.call.number;
					this.origin = event.call.origin;
				}
				
				this.causeCode = event.causeErrorCode;
				this.causeErrorText = event.causeErrorText
        	}
			
			if (this.action == TelephonyCallState.DIALING) {
				if (this.disconnectDialPendingCall) {
					// if we have a disconnect pending for a call we haven't
					// received an id for, kill it
					this.onDisconnectDialPendingCall(this.affectedCallId);
				} else {
					// convert the service's dialing state into one that 
					// more resembles a call
					this.mungeDialingState(serviceMessage.macroEventCalls);
				}
			} 
			
			// we have a call event, so parse it and store it
			if (serviceMessage.macroEventCalls) {
				var lines = this.parseEvent(serviceMessage.macroEventCalls);
				
				// if we were resurrected, pretend to be active 
				// if there were any calls
				if (this.action == TelephonyCallState.QUERY) {
					if (lines.length > 0) {
						this.action = TelephonyCallState.ACTIVE;
					}
				// if we had dialpending, copy it in if it hasn't been replaced
				// by a real dialing event
				} else if (this.action != TelephonyCallState.DIALING) {
					this.copyDialPending(lines);
					this.copyDisconnectedCall(lines);
				}
				// save the calls
				this.lines = lines;
			}
		}
	},
	
	// TODO: make this generate a TelephonyCallState instead of a service message
	// makes a fake dialing message containing the number 
	conjureDialingMessage: function(dialParams){
		QDLogger.log( "TCS::conjureDialingMessage");
		
				
		// don't do this if we have 2 lines already
		if (!dialParams || !(dialParams.number) || this.lines && this.lines.length == 2)
			return;
		
		var number = dialParams.number;
		
   		//the singleDialing object needs an "id"
		//which we haven't yet received from the TIL
		//the phone number should work as a unique identifier
		var c = {
			"cnapName": "",
			id: number,
			"number": number,
			origin: TelephonyCallState.DIALING,
			counter: 999, // TODO a really high number
			state: TelephonyCallState.DIALPENDING,
			contact: this.contacts.getContact(number, dialParams)
		};
		
	
		this.previousAction = this.action;
		this.action = TelephonyCallState.DIALPENDING;
		if (this.lines.length == 1 && this.lines[0].state == TelephonyCallState.DISCONNECTED) {
			this.lines[0] = c;
		} else {
			this.lines[this.lines.length] = c;
		}
		this.number = number;		
	},
	
	mungeDialingState: function(m) {
		QDLogger.log( "TCS::mungeDialingState");

        // grab parameters
		var cnapName = m.eventCall.eventCalls.call.cnapName;
        var callId = m.eventCall.eventCalls.call.id;
        var number = m.eventCall.eventCalls.call.number;
        var origin = m.eventCall.eventCalls.call.origin;
        var counter = m.eventCall.eventCalls.call.counter;
		
		m.singleDialing = {
			"cnapName": cnapName,
			"id": callId,
			"number": number,
			"callState": TelephonyCallState.DIALING, 
			"origin": origin,
			"counter": counter
		};
		
	},
	
	// if there's a previous dialpending call, and it hasn't been replaced by a 
	// real dialing call, and the number of calls hasn't changed, copy it in
	copyDialPending: function(lines) {
		QDLogger.log( "copyDialPending");
		
		if (!(lines) 
			|| lines.length === 0) {
				QDLogger.log( "copyDialPending","NO CALLS EXIST");
				return;
			} 
			
		if (this.lines.length == lines.length + 1 // new number of calls should only be 1 lower since we didn't copy in dialing yet
			&& this.lines[this.lines.length-1].state == TelephonyCallState.DIALPENDING
			&& lines[lines.length-1].state !== TelephonyCallState.DIALING) {
				lines.push(this.lines[this.lines.length-1]);
		}
	},
	
	// finds disconnected calls from the previous line state
	// and copies them in.  
	copyDisconnectedCall: function(lines) {
		QDLogger.log( "copyDisconnectedCall");
		// if this is the last member call in a conference being disconnected
		// fake this in as a conference call rather than a single disconnected
		// call
		if (this.lines.length == 1
			&& lines.length == 0 
			&& this.action == TelephonyCallState.DISCONNECTED
			&& this.previousAction == TelephonyCallState.CONFERENCE_ACTIVE) {
			this.addDisconnectedConference(lines, this.affectedCallId, this.number, this.lines[0].origin);
		// otherwise if there's one fewer call in this event than in the last event
		// see if there are any calls suitable for copying
		} else if (this.lines.length == lines.length + 1) {
			var lineToCopy = -1;
			
			// on CDMA, if this is a disconnect action, and we had 2 calls before
			// and 1 call now, copy both calls in, but mark them disconnected
			if (PlatformType.isGSM() == false 
				&& this.action == TelephonyCallState.DISCONNECTED
				&& this.lines.length == 2
				&& lines.length == 1) {
				lines[0] = this.lines[0]; 
				lines[0].state = TelephonyCallState.DISCONNECTED;
				
				lines[1] = this.lines[1];
				lines[1].state = TelephonyCallState.DISCONNECTED;
				
				return;
			}
			
			// look for disconnectpending calls and already disconnected calls
			for (var i = 0; i < this.lines.length; i++) {
				if (this.lines[i].state == TelephonyCallState.DISCONNECTED 
					|| this.lines[i].state == TelephonyCallState.DISCONNECTPENDING) {
					lineToCopy = i;
					break;
				}
			}
			
			// if we didn't find any already disconnected calls, 
			// and this is a disconnect event look for missing calls
			if (lineToCopy === -1 && this.action == TelephonyCallState.DISCONNECTED) {
				for (var i = 0; i < this.lines.length; i++) {
					var matched = false;
					for (var j = 0; j < lines.length; j++) {
						if (i.id == j.id) {
							matched = true;
							break;
						}
					}
					if (!matched) {
						lineToCopy = i;
						break;
					}
				}
			}
			
			// if we found a call, copy it in
			if (lineToCopy > -1) {
				if (lineToCopy == 0) {
					QDLogger.log( "copyDisconnectedCall", "line 0");
					lines.splice(0, 0, this.lines[0]);
				} else if (lineToCopy == 1) {
					lines.push(this.lines[1]);
				}
				
				// if this is a disconnect event, assume the line we copied in
				// should be flagged as disconnected 
				if (this.action == TelephonyCallState.DISCONNECTED) {
					lines[lineToCopy].state = TelephonyCallState.DISCONNECTED;
				}
			}
		}
	},
	
	// given a macroCall event, fills this.lines with an array of objects for 
	// active and held calls containing their properties
	parseEvent: function(m) {
		var lines = [];
		var singleStates = ['singleActive', 'singleHold', 'singleDialing'];
		var confStates = ['conferenceActive', 'conferenceHold'];
		QDLogger.log( "parseEvent");
		
		// add single calls
		for (var i = 0; i < singleStates.length; i++) {
			if (m[singleStates[i]]) {
				QDLogger.log( "parseServiceMessage","adding call " , singleStates[i]);
				this.addCall(lines, m[singleStates[i]]);
			}
		}
		
		// add conference calls
		for (var i = 0; i < confStates.length; i++) {
			if (m[confStates[i]]) {
				QDLogger.log( "parseServiceMessage","adding conference");
				// TODO FIXME: workaround extracted conference member being flagged as conference
				if (m[confStates[i]].size > 1) {
					this.addConference(lines, m[confStates[i]]);
				} else {
					QDLogger.log( "parseServiceMessage","actually not a conference");
					this.addCall(lines, m[confStates[i]].calls);
				}
			}
		}
		
		// keep order consistent
		this.maintainOrder(lines);
		
		return lines;
		
	},
	
	// puts the call list in the 'right' order
	// right means: conference call first.  if no conference call, then
	// sort on counter
	maintainOrder: function(lines) {
		
		// if the first call is a conference, don't do anything
		if (lines.length < 2
			|| lines[0].confId)
			return;

		// if the second call is a conference, or the second call has a lower
		// sequence number, swap
		if (lines[1].confId || (lines[0].counter > lines[1].counter)) {
			c = lines[1];
			lines[1] = lines[0];
			lines[0] = c;
		}
	},
	
	// given an array of calls and a member call from a macro call object
	// pushes an object onto the array containing properties of the call
	addCall: function(callList, call) {
		if (call.id) {
			QDLogger.log( "addCall","adding call");
			
			var newCall = {
				id: call.id,
				state: this.normalizeState(call.callState),
				number: call.number,
				//number: "unknown",
				cnapName: call.cnapName,
				origin: call.origin,
				counter: call.counter,
				conferenceMember: (call.conferenceId === -1)
			};
			
			this.addStartTime(newCall);
			this.addContactInfo(newCall);
			callList.push(newCall);
		}
		
	},
	
	// given an array of calls and a member conferencecall from a macro call object
	// pushes an object onto the array containing properties of the call and
	// a sublist of calls
	addConference: function(callList, call) {
		// size implies a hopefully valid conference
		if (call.size && call.size > 1) {
			QDLogger.log( "addConference","adding call");
			var memberCalls = [];
			var masterCall = {};
			
			// if somehow there's no call here, then just grab call info
			// from the call passed in instead of the first conference call
			// member
			if (!(call.calls) || call.calls.length < 1 ) {
				masterCall = call;
			} else {
				masterCall = call.calls[0];
				// grab the calls in the conference
				for (var i = 0; i < call.calls.length; i++) {
					this.addCall(memberCalls, call.calls[i]);
				}
			}
			
			// TODO sort member calls
			var newCall = { 
				// the global conference id
				id: this.conferenceId,
				// assume hold if not active
				// assume all calls have the same state
				state: this.normalizeState(masterCall.callState),
				// assume all calls have the same conferenceId
				confId: masterCall.conferenceId,
				calls: memberCalls,
				// add dummy contact -- no lookup, so just say it's done
				contact: { lookupComplete: true, manuallySet: true }
			}
			
			this.addStartTime(newCall);
			callList.push(newCall);
			
		} else {
			QDLogger.log( "parseServiceMessage","not a valid conference");
		}
	}, 
		
	// adds a fake disconnected conference call; used when the last call is disconnected
	// TODO: FIXME: this is not we want. aside from this being an odd special case,
	// to make sure disconnect tracking works, we load the call's id and number in
	addDisconnectedConference: function(callList, lastMemberCallId, lastMemberNumber, lastMemberOrigin) {
		var fakeDisconnectedConferenceCall = {
			id: lastMemberCallId,
			state: TelephonyCallState.DISCONNECTED,
			origin: lastMemberOrigin,
			confId: this.conferenceId,
			calls: [],
			number: lastMemberNumber,
			contact: this.contacts.getContact(lastMemberNumber)
		}
		
		callList.push(fakeDisconnectedConferenceCall);
	},
	
	// reduce conference states to non-conference states
	normalizeState: function(state) {
		if (state == TelephonyCallState.CONFERENCE_ACTIVE) {
			return TelephonyCallState.ACTIVE
		} else if (state == TelephonyCallState.CONFERENCE_HOLD) {
			return TelephonyCallState.HOLD
		} else {
			return state;
		}
	},
	
	// given a call from the calllist, adds a start time by looking for the
	// call's previous start time or making a new one, but only for active calls
	addStartTime: function(call) {
		var oldCall = this.getCallWithId(this.lines, call.id);
		if (oldCall && oldCall.startTime) {
			call.startTime = oldCall.startTime;			
			QDLogger.log( "addStartTime","OLD START TIME");
			return;
		} else if (call.state == TelephonyCallState.ACTIVE) {
			QDLogger.log( "addStartTime","NEW START TIME");
			call.startTime = new Date().getTime();				
		}
	},

	// given a call from the calllist, adds info from contacts by looking for the
	// call's previous contact info or by making a new empty object to be filled
	// later
	addContactInfo: function(call) {
		var oldCall = this.getCallWithId(this.lines, call.id);
		if (oldCall && oldCall.contact) {
			call.contact = oldCall.contact;			
			return;
		} else {
			call.contact = this.contacts.getContact(call.number);
		}
		
	},
	
	// looks through a passed array of calls for a call object with a given id
	getCallWithId: function(list, id) {
		QDLogger.log( "TelephonyCallState::getCallWithId");
		for (var i = 0; i < list.length; i++) {
			if (list[i].id == id) {
				return list[i]
			} else if (list[i].confId) {
				var childId = this.getCallWithId(list[i].calls, id);
				if (childId)
					return childId;
			}
		}
	},
	
	// given the id of a call, returns the starttime of the call 
	// by searching the list of active/held calls for the call  
	getStartTime: function(id) {
		// check lines for this call
		for (var i = 0; i < this.lines.length; i++) {
			if (this.lines[i].id == id) {
				return this.lines[i].startTime
			// or if it's a conference, the list of calls in the conference
			} else if (this.lines[i].confId) {
				var call = this.getCallWithId(this.lines[i].calls, id);
				if (call)
					return call.startTime;
			} 
		}		
	},
	
	isDialing: function() {
		for (var i = 0; i < this.lines.length; i++) {
			if (this.lines[i].state == TelephonyCallState.DIALING
				|| this.lines[i].state == TelephonyCallState.DIALPENDING)
				return true;
		}
		return false;
	},
	
	isConnected: function() {
		QDLogger.log( "TelephonyCallState::isConnected");
		if (this.lines.length > 0)
			if (this.isDialing())
				return ( (this.lines.length - 1) > 0);
			else
				return (this.lines.length > 0);
		else
			return false;
	},
	
	// returns true if there's a call in any state except disconnected
	isPendingOrActive: function() {
		for (var i = 0; i < this.lines.length; i++) {
			if (this.lines[i].state !== TelephonyCallState.DISCONNECTED)
				return true;	
		}
		return false;
	},
	
	callExists: function() {
		return (this.lines.length > 0);
	},
	
	// removes calls waiting for radio to indicate are being dialed
	clearDialPending: function() {
		QDLogger.log( "TelephonyCallState::clearDialPending");
		// ASSume that it could only possibly be the last call
		this.clearDialPendingDisconnect();
		var newLines = [];
		
		if (this.lines.length == 0) {
			QDLogger.log( "TelephonyCallState::clearDialPending", "no calls!");
			return;
		}
		
		var lastCall = this.lines[this.lines.length-1];
		// if the last call is in dial pending or disconnect pending, then clear it
		// this is sort of a workaround, but seems reasonable; if the last call
		// is in disconnect pending, the user probably intends for it to go away
		// in the event of a call failure
		if (lastCall.state == TelephonyCallState.DIALPENDING
			|| lastCall.state == TelephonyCallState.DISCONNECTPENDING) {
			if (this.lines.length > 2) {
				QDLogger.log( "TelephonyCallState::clearDialPending", "> 2 lines!: " , this.lines.length);
				for (var i = 0; i < this.lines.length-1; i++) {
					newLines[i] = this.lines[i];
				}
				this.action = TelephonyCallState.ACTIVE;
			} else if (this.lines.length == 2) {
				newLines[0] = this.lines[0];
				this.action = TelephonyCallState.ACTIVE;
			} else {
				this.action = TelephonyCallState.DISCONNECTCLEAR;
			}
		// otherwise there's no call in dialpending, so just copy the old calls in
		} else  {
			QDLogger.log( "TelephonyCallState::clearDialPending", "last call in state: " , this.lines[this.lines.length-1].state);
			for (var i = 0; i < this.lines.length; i++) {
				newLines[i] = this.lines[i];
			}
		}
		this.lines = newLines;
	},
	
	// clears out calls.  called when radio crashes or is suddenly powered off
	clearCalls: function () {
		this.action = TelephonyCallState.CRASH;
		this.lines = [];
		return this;
	},
	
	// drops a phone call.  handles dialpending calls (which haven't yet been assigned an ID by the TIL)
	// by queuing a disconnect 
	disconnectCall: function(linenumber) {
		QDLogger.log( "TelephonyCallState::disconnectCall");
		
		// if the line doesn't exist, don't continue
		if (linenumber > this.lines.length - 1)
			return this;
		
		// ignore disconnects if we've already done it
		if (this.lines[linenumber].state == TelephonyCallState.DISCONNECTED
			|| this.lines[linenumber].state == TelephonyCallState.DISCONNECTPENDING)
			return this;
		
		// if we are in dialpending queue the disconnect
		if (this.lines[linenumber].state == TelephonyCallState.DIALPENDING) {
			this.disconnectDialPendingCall = true;
		// otherwise do it
		} else {
			var id = this.lines[linenumber].id;
			if (id === this.conferenceId) {
				TelephonyCommands.disconnectConference();
			} else {
				TelephonyCommands.disconnect(id);
			}
		}
		// flag this as a pending disconnect
		this.lines[linenumber].state = TelephonyCallState.DISCONNECTPENDING;
		
		return this;
	},
	
	clearDialPendingDisconnect: function() {
		this.disconnectDialPendingCall = false;
	},
	
	// assumes the only call that could be dialing is the last one
	onDisconnectDialPendingCall: function(id) {
		QDLogger.log( "TelephonyEventListener::onDisconnectDialPendingCall"); 
		this.clearDialPendingDisconnect();
		TelephonyCommands.disconnect(id);
	},
	
	// removes a disconnected call from the list and returns the object again,
	// now lacking it
	clearDisconnectedCall: function() {
	
		QDLogger.log( "TelephonyCallState::clearDisconnectedCall");
		if (this.lines.length == 1 && this.lines[0].state == TelephonyCallState.DISCONNECTED) {
			this.lines = [];
		} else if (this.lines.length == 2) {
			if (this.lines[0].state == TelephonyCallState.DISCONNECTED) {
				this.lines = [this.lines[1]];
			} else if (this.lines[1].state == TelephonyCallState.DISCONNECTED) {
				this.lines = [this.lines[0]];
			}
		} else {
			QDLogger.log( "TelephonyCallState::clearDisconnectedCall", "can't find disconnected call");
		}
		
		this.action = TelephonyCallState.DISCONNECTCLEAR;
		return this;
	
	},
	
	
});

TelephonyCallState.INCOMING = "incoming";
TelephonyCallState.DIALING = "dialing";
TelephonyCallState.DIALPENDING = "dialpending";
//TelephonyCallState.SINGLE_ACTIVE = "single_active";
//TelephonyCallState.NONE = "none";
//TelephonyCallState.CONFERENCE = "conference";
//TelephonyCallState.DUAL_LINE = "dual";
TelephonyCallState.ACTIVE = "active";
TelephonyCallState.CONFERENCE_ACTIVE = "conference_active";
TelephonyCallState.DISCONNECTCLEAR = "disconnectclear"
TelephonyCallState.DISCONNECTPENDING = "disconnectpending";
TelephonyCallState.DISCONNECTED = "disconnected";
TelephonyCallState.MERGED = "merged";
TelephonyCallState.EXTRACTED = "extracted";
TelephonyCallState.HOLD = "hold";
TelephonyCallState.CONFERENCE_HOLD = "conference_hold";
TelephonyCallState.INFO = "info";
TelephonyCallState.CRASH = "crash";
TelephonyCallState.QUERY = "query";