/* ***************************************************************************+
 * ITX package (cnrg.itx) for telephony application programming.              *
 * Copyright (c) 1999  Cornell University, Ithaca NY.                         *
 * A copy of the license is distributed with this package.  Look in the docs  *
 * directory, filename GPL.  Contact information: bergmark@cs.cornell.edu     *
 ******************************************************************************/

package cnrg.itx.signal;

import cnrg.itx.ds.*;
import cnrg.itx.datax.*;
import cnrg.itx.signal.SignalEvent.*;

import java.net.*;
import java.io.*;

/** This class contains the functionality to dial a single location and is created by a
 *  DesktopSignaling component.
 */
class DialThread extends Thread
{
	/***************** Callee Related Information and objects *******************/
	
	/** Destination userID **/
	private UserID myDestUID = null;
	/** Destination location **/
	private Location myDestAdd = null;
	/** Data exchange connection to use **/
	private Connection myC = null;
	/** Invite packet to be sent to the peer application **/
	private InvitePacket myIP = null;
	
	/*****************  Object Handles *******************/
	
	/** Application (that implements SignalConnectionObserver) handle **/
	private SignalConnectionObserver myApp = null;
	/** SignalConnection to use **/
	private SignalConnection mySC = null;
	/** DesktopSignaling handle **/
	private DesktopSignaling myDS = null;
	
	/***************** Internal Variables  *******************/
	
	/** Communcation socket **/
	private Socket sendSock = null;
	/** To run or not to run **/
	private boolean flag = true;
	/** Sequence number for this connection **/
	private Long seq = null;
	
	/** Constructor
	 * 
	 * @param duid the userid of the destination
	 * @param dl the location of the dstination
	 * @param c the connection to use for data exchange
	 * @param sco the application handle to inform the app about the dial events
	 * @param sc the handle to the SignalConnection
	 * @param ds the handle to DesktopSignaling
	 */
	public DialThread(UserID duid, Location dl, Connection c, SignalConnectionObserver sco, SignalConnection sc, DesktopSignaling ds){
		myDestUID = duid;
		myDestAdd = dl;
		myC = c;
		myApp = sco;
		mySC = sc;
		myDS = ds;
	}
		
	/**
	 * Method called when the thread is started.
	 * 
	 * @param   None.
	 * @return  void
	 */
	public void run(){
		DialSignalEvent dse = null;
		int res;
		
		if (flag){
			try {
				res = tryDialSequence(myDestUID, myDestAdd, myC);
			}
			catch(ConnectException ce){
				ce.printStackTrace();
				myC.close();
				myApp.onError(ce);
				return;
			}						
			switch(res){
				case SignalID.ACCEPT:
					mySC.setCurrentState(mySC.CONNECTED);						
					myDS.myConnectionList.put(seq, mySC);
					dse = new DialSignalEvent(myIP, mySC, myDestAdd);											
					myApp.onAccept(dse);					
					mySC.startKeepAlive(myDS);
					break;
				case SignalID.REJECT:
					myC.close();
					mySC.setCurrentState(mySC.IDLE);
					dse = new DialSignalEvent(myIP, mySC, myDestAdd);											
					myApp.onReject(dse);												
					break;
				case SignalID.INVALID:
					myC.close();
					mySC.setCurrentState(mySC.IDLE);
					dse = new DialSignalEvent(myIP, mySC, myDestAdd);											
					myApp.onInvalid(dse);													
					break;
				case SignalID.BUSY:
					myC.close();
					mySC.setCurrentState(mySC.IDLE);
					dse = new DialSignalEvent(myIP, mySC, myDestAdd);					
					myApp.onBusy(dse);						
					break;
				case SignalID.TIMEOUT:
					myC.close();
					mySC.setCurrentState(mySC.IDLE);
					dse = new DialSignalEvent(myIP, mySC, myDestAdd);					
					myApp.onTimeout(dse);												
					break;
				case SignalID.INCOMPATIBLE:
					myC.close();
					mySC.setCurrentState(mySC.IDLE);
					dse = new DialSignalEvent(myIP, mySC, myDestAdd);					
					myApp.onIncompatible(dse);												
					break;
			}//switch				
		}//if
		System.out.println("Dialer Thread exiting for user: " + myDestUID.toString());
	}
	
	
	/**
	 * This method kills the thread by closing the listening socket and exiting the run() method
	 * 
	 * @param   None.
	 * @return  void
	 */
	public void cleanup(){
		flag = false;
		try{
			if(sendSock != null)
				sendSock.close();
			else
				System.out.println("Socket is null.");
		}
		catch(IOException ioe){
			System.out.println("Dialer Thread interrupted by user.  Aborting call...");
		}
		System.out.println("Dial Thread Cleanup complete.");
	}

	/** This method performs a Dial Sequence including the 3-way handshake.  The result of the hadnshake is returned
	 *  to the callee method.
	 * 
	 * @param destUID the userid of the destination
	 * @param destAdd the location of the dstination
	 * @param ac the connection to use for data exchange 
	 */
	private int tryDialSequence(UserID destUID, Location destAdd, Connection ac) throws ConnectException
	{
		Long peerseq = null;
		
		System.out.println("Dial-> Source: " + myDS.myUID.toString() + " & Dest: " + myDestUID.toString());
		
		myIP = new InvitePacket(myDS.myUID, myDS.myLoc, myDS.myDesc, SignalID.DIAL);
		PropertiesCollection pc = ac.getProperties();
		myIP.setPropertiesCollection(pc);
		myIP.setDestination(destUID);

		try{
			sendNewPacket(myIP, destAdd.getIP(), destAdd.getPort());
		}
		catch(ConnectException ce){
			throw ce;
		}
		
		try{
			myIP = (InvitePacket) waitForPacket(sendSock);
		}
		catch(InterruptedIOException iioe){
			try{
				sendSock.close();
			}
			catch(IOException ioe){
				ioe.printStackTrace();
			}			
			return SignalID.TIMEOUT;
		}
		
		try{
			if (myIP.isResultPacket()){
				if(myIP.wasAccepted()){
					pc = myIP.getPropertiesCollection();
					ac.setPeerProperties(pc);
					peerseq = myIP.getSeqNumber();
					mySC.setConnection(ac);
					mySC.setIP(myDestAdd.getIP());
					mySC.setPort(destAdd.getPort());
					seq = new Long(myDS.myConnSeqNumber);
					myDS.myConnSeqNumber++;
					mySC.setSeqNumber(seq);
					mySC.setPeerSeqNumber(peerseq);
					myDS.myConnectionList.put(seq, mySC);	
					
					myIP.confirm();
					myIP.setPacketType(SignalID.CONFIRM);
					myIP.setSeqNumber(seq);//send my seq number
					sendPacket(myIP, sendSock);
					sendSock.close();
					return SignalID.ACCEPT;
				}
				else if(myIP.wasRejected()){
					sendSock.close();
					return SignalID.REJECT;
				}
				else if(myIP.wasBusy()){
					sendSock.close();
					return SignalID.BUSY;
				}					
			}
			else{//Wrong Sequence/Packet
				sendSock.close();
				return SignalID.INVALID;
			}
		}
		catch (IOException ioe){
			ioe.printStackTrace();
		}
		catch(DataException de){
			de.printStackTrace();
		}
		
		return SignalID.INVALID;
	}
	
	/** This method sends a packet over a TCP connection.  A new socket is created.
	 * 
	 * @param   send is the SigPacket to send to the peer application
	 * @param   ip is the String representation of the IP address of the destination
	 * @param   p is the port of the destination application
	 * 
	 * @return  TRUE if packet was successfully sent, FALSE otherwise
	 * 
	 * @exception DesktopSignalingException
	 */
	private boolean sendNewPacket(SigPacket send, String ip, int p) throws ConnectException{		
		InetAddress add = null;
		try{
			add = InetAddress.getByName(ip);
		}
		catch(UnknownHostException uhe){
			uhe.printStackTrace();
		}
		
		try{
			sendSock = new Socket(add, p);
			sendSock.setSoTimeout(myDS.TIMEOUT);
			DataOutputStream f = new DataOutputStream(sendSock.getOutputStream());
			ObjectOutputStream s = new ObjectOutputStream(f);
		
			if (send != null)
				s.writeObject(send);
			else 
				throw new ConnectNullObjectSentException("Error: Null object being sent to peer");
			s.flush();
		}
		catch(IOException ioe){
			throw new ConnectFailedToOpenSocketException("Socket connection to " + add + ":" + p + " failed.");
		}		
		System.out.println("Sent SigPacket to " + ip + ":" + p + "\n");
		return true;
	}
	
	/** This method sends a packet over an TCP socket.  No new socket is created.
	 * 
	 * @param   send is the SigPacket to send to the peer application
	 * @param   sock is the existing socket to use.
	 * @return  void
	 * @exception DesktopSignalingException
	 */
	private void sendPacket(SigPacket send, Socket sock) throws ConnectException{			
		if (sock == null)
			throw new ConnectNullSocketException("Error:  Cannot send packets on a null socket!\n");
		try{
			DataOutputStream f = new DataOutputStream(sock.getOutputStream());
			ObjectOutputStream s = new ObjectOutputStream(f);
		
			if (send != null)
				s.writeObject(send);
			else 
				throw new ConnectNullObjectSentException("Error: Null object being sent to peer");		
			s.flush();
		}
		catch(IOException ioe){
			ioe.printStackTrace();
		}
		System.out.println("Sent SigPacket on connected socket\n");
	}
	
	private SigPacket waitForPacket(Socket sock) throws InterruptedIOException{
		Object obj = null;
	
		try {
			DataInputStream in = new DataInputStream(sock.getInputStream());
			ObjectInputStream s = new ObjectInputStream(in);
			obj = s.readObject();
		} 
		catch (ClassNotFoundException ce) {
			ce.printStackTrace();
		}
		catch (InterruptedIOException iioe){
			try{
				sendSock.close();
			}
			catch(IOException ioe){
				ioe.printStackTrace();
			}
			throw iioe;
		}
		catch(IOException ioe){
			throw new InterruptedIOException("Socket at destination has been closed.  Trying next item on list.\n"); 
		}		
		return (SigPacket) obj;
	}
}
