// bergmark - Telephony project - Spring 1999
package cnrg.itx.gtwy.pbx;
/* ***************************************************************************+
 * 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     *
 ******************************************************************************/
 
// The dialer runs in its own thread and performs actions on the PBX,
// whose provider is gotten from the PBX Signaling object that invoked
// the dialer.  The dialer also processes hangup requests.  After
// successfully placing a call, the dialer suspends itself.  It will be
// resumed by PBX Signaling when (1) a hangup request for that
// call comes in; (2) the user callee hangs up (To Be Implemented); and
// (3) when the PBX Signaling Server is shut down.

// A point of contention is "when should and accept packet be sent back
// to the gateway?"  One answer is: as soon as a call has been successfully
// placed (i.e. terminal is ringing).  Set WAIT to false to do this option.
// Another answer is to wait for a certain time of rining before sending
// back a Reject (set WAIT to true).

// Dialer class based on code written for CS 519, Fall 1998, by Mahvad Ranjan
// and Abhideep Singh.  

import javax.telephony.*;
import javax.telephony.events.*;
import javax.telephony.callcontrol.*;

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

import cnrg.itx.signal.InvitePacket;
import cnrg.itx.signal.SignalID;
import cnrg.itx.signal.SigPacket;
import cnrg.itx.ds.Location;

/**
 * This class handles placing one call in the PBX, from a line in the
 * Gateway to some telephone number specified by a caller
 */
public class Dialer extends Thread {

    private static final String ME = "Dialer: ";
    private boolean LOG = false;

    private static final boolean WAIT = false; // accept when ringing
    public static final int TIMEOUT = 20000;  // 20 seconds for ringing

    private Connection  myConnection;
    private Connection  destConnection;
    private Terminal    terminal;
    private Provider    provider;
    private Call        call = null;
    private CallObserver co = null;
    private String      calling;
    private String      called;
    private boolean     callInP = false;// call in progress
    private boolean     dialingInP = false; // dialing in progress
    private PBXSignaling myPBX = null;  
    private InvitePacket myPacket = null; // set by constructor
    private InetAddress myIAdd = null;    // PBX Server's inet addr
    private Socket      sendSock=null; // socket to our gateway

    /** Base constructor for Dialer class
     * @param p is the PBXSignaling object that is creating this Dialer
     * @param logging is TRUE if PBX Server is logging to the terminal
     * @param sp is the InvitePacket that contains a dial request
     * @param from is the telephone number from which call will be place
     * @param to is the telephone number to be called
     * @param gs is the Socket over which our Gateway is talking to us
     * @exception DialingException thrown in there is no provider
     */
    public Dialer(PBXSignaling p, 
		  boolean logging, 
		  InvitePacket sp, 
		  String from, 
		  String to, 
		  Socket gs) 
    throws DialingException {
       myPacket = sp;
       try {
          myIAdd = InetAddress.getLocalHost();
       } catch (UnknownHostException e){ 
	  System.out.println (ME+e.toString());
	  e.printStackTrace();
       }
       this.calling = from;
       this.called = to;
       this.myPBX = p;
       this.provider = p.getProvider();
       sendSock = gs;
       if ( provider == null ) {
	  throw new DialingException (ME +  " PBX Signaling has no provider " );
       }
       LOG = logging;
       if (LOG) System.out.println (ME + "logging turned on for Dialer");
    }

    /** Constructor with default logging (which is none)
     * @param p is the PBXSignaling object that is creating this Dialer
     * @param sp is the InvitePacket that contains a dial request
     * @param from is the telephone number from which call will be place
     * @param to is the telephone number to be called
     * @param gs is the Socket over which our Gateway is talking to us
     * @exception DialingException thrown in there is no provider
     */
    public Dialer(PBXSignaling p, 
       InvitePacket sp, String from, String to, Socket gs)
    throws DialingException {
        this (p, false, sp, from, to, gs);
    }

   /*
    * makeCall()
    * This method creates Terminal, Address and Call objects.  It adds a callObserver
    * to the call, and calls the connect() method of the Call object
    * @param  from   calling telephone number (normally in gateway)
    * @param  to     number to be dialed
    * @exception DialingException thrown if there are any problems
    * created Terminals, Addresses, Connections, or a Call
    */
    private void makeCall(String from, String to) 
    throws DialingException {
        javax.telephony.Address         address = null;
        Connection[]    connections = null;

        String          callingExt;
        String          calledExt;
        
        callingExt = from;
        calledExt = to;
	dialingInP = true;

        if (LOG) System.out.println (ME + callingExt + " is calling " + calledExt );

	// provider is guaranteed to be non-null by the constructor
        // create Address
        try {
           address = provider.getAddress( callingExt );
        } catch (InvalidArgumentException e) {
           throw new DialingException 
              ("Address " + callingExt + " could not be created because "
	      +"Provider.getAddress() caught InvalidArgumentException\n\n");
        } catch (PlatformException e) {
           throw new DialingException 
              ("Address " + callingExt + " could not be created because "
              +"Provider.getAddress() caught PlatformException\n\n");
        }

	// If we get to here, the Address was successfully created
        if (LOG)
           System.out.print(ME+"Address " + callingExt + " created successfully\n\n");

        // create Terminal
        try {
           terminal = provider.getTerminal(callingExt);
        } catch (InvalidArgumentException e) {
           throw new DialingException
              ("Terminal "+callingExt+
	      " could not be created because Provider.getTerminal() "
	      + "caught InvalidArgumentException\n\n");
        } catch (PlatformException e) {
           throw new DialingException
              ("Terminal "+callingExt+
	      " could not be created because Provider.getTerminal() "
              + "caught PlatformException\n\n");
        }

	// If we got to here, we got a terminal
        if (LOG)
           System.out.print(ME +
              "Terminal " + terminal.getName() + " created successfully\n\n");

       // Create a call
       try {
          call = provider.createCall();
       } catch (InvalidStateException e) {
          throw new DialingException ("could not create a call because "
             + "Provider.createCall(): caught InvalidStateException\n\n");
       } catch (MethodNotSupportedException e) {
          throw new DialingException ("could not create a call because "
	     + "Provider.createCall() caught MethodNotSupportedException\n\n");
       } catch (ResourceUnavailableException e) {
          throw new DialingException ("could not create a call because "
	     + "Provider.createCall() caught ResourceUnavailableException\n\n");
       } catch (PlatformException e) {
          throw new DialingException ("could not create a call because "
             + "Provider.createCall(): caught PlatformException\n\n");
       } catch (PrivilegeViolationException e) {
          throw new DialingException ("could not create a call because "
             + "Provider.createCall(): caught PrivilegeViolationException\n\n");
       }
       // If we get to here we have a call
       if (LOG)
          System.out.print(ME + "Call created successfully\n\n");

       // Create a Call Observer (if we can) and add it to the call
/* Skip it ... see if number of jtapi crashes goes down (it didn't)
       co = new MyCallObserver ( LOG );
       try {
          call.addObserver (co);
          if (LOG) {
             CallObserver[] obs = call.getObservers();
             System.out.print (ME + "this call to " + calledExt +
	        " now has " + obs.length + " call observer" );
             System.out.println ( obs.length == 1 ? "." : "s." );
          }
       } catch ( Exception e ) {
	  if ( LOG ) 
	     System.out.println ( ME + "couldn't get call observer because " 
		+ e.toString() );
       }
*/

       // makecall by using the connect method of the Call object
       // Call will be in Invalid State until calling terminal is placed offhook
       if (LOG)
          System.out.print(ME + "Making Call from " + callingExt + " to " + 
             calledExt + "...\n\n");
       try {
           connections = makeOneCall (terminal, address, calledExt, call );
       } catch ( InvalidStateException e ) {
	   if (LOG)
              System.out.print( ME + "Call.connect: caught InvalidStateException\n\n" );
           System.out.println (ME + "Put " + terminal.getName() + " offhook:" );
           // the following will either get connection or throw DialingExcept
           connections = tryMoreTimes ( 5, terminal, address, calledExt, call );
       } 
       // if we get to here, we have the connections
       if ( connections == null )
          throw new DialingException ("assert failure in makeCall-- " +
	     "connection null");
       if (LOG) System.out.println ( ME + "got a connection!" );

       // the first connection created represents the originating end of the call
      // the second connection created represents the destination end of the call
        myConnection = connections[0];
        destConnection = connections[1];

    }

   /**
    * This method makes 5 more attempts to place the call before
    * throwing a DialingException
    * @param n is the number of tries to make
    * @param t is the Terminal that is making the call
    * @param a is the Address corresponding to <code>t</code>
    * @param s is the String form of the destination telephone number
    * @param c is the Call whose connect method we will call
    * @returns Connection[] of two connections, if successful
    * @exception DialingException if caller does not go off-hook
    */
   private Connection[] tryMoreTimes ( int n,
      Terminal t, Address a, String s, Call c ) throws DialingException {
      for ( int i = 0; i < n; i++ ) {
         try {
            return makeOneCall ( t, a, s, c );
         } catch ( InvalidStateException exp) {
           System.out.println (ME + "Put " + terminal.getName() + " offhook:" );
         } // if other error, makeOneCall threw DialingException
      }
      // Come here after n more attempts to do a connection 
      if (LOG) 
         System.out.println(ME + n + " more tries at conn-making exhausted");
      throw new DialingException ("could not make a connection "
                 + "after " + n + " more attempts.\n\n");
   } // ends tryMoreTimes


   /**
    * This method makes one attempt at building a connection.  First it
    * checks to see that our provider is in service and makes one attempt
    * to resuscitate it if it is not in service.
    * @param t is the Terminal that is making the call
    * @param a is the Address corresponding to <code>t</code>
    * @param s is the String form of the destination telephone number
    * @param c is the Call whose connect method we will call
    * @exception DialingException if Call.connect() fails
    * @exception InvalidStateException if caller is not off-hook
    * @returns Connection[] of two connections, if successful
    */
   private Connection[] makeOneCall ( Terminal t, Address a, String s, Call c )
   throws DialingException, InvalidStateException {
     if ( myPBX.checkProvider() ) 
       try {
          return c.connect ( t, a, s );
       } catch ( InvalidStateException e ) { 
          throw e;  // Could mean caller is not off hook yet
       } catch ( MethodNotSupportedException e ) {
           throw new DialingException ("could not make a connection "
              + "because Call.connect caught MethodNotSupportedException\n\n" );
       } catch ( InvalidArgumentException e ) {
           throw new DialingException ("could not make a connection "
              + "Call.connect: caught InvalidArgumentException\n\n" );
       } catch ( ResourceUnavailableException e ) {
           throw new DialingException ("could not make a connection "
              + "because Call.connect caught ResourceUnavailableException\n\n" );
       } catch ( PlatformException e ) {
           throw new DialingException ("could not make a connection "
              + "because Call.connect caught PlatformException\n\n" );
       } catch ( PrivilegeViolationException e ) {
           throw new DialingException ("could not make a connection "
              + "because Call.connect caught PrivilegeViolationException\n\n" );
       } catch ( InvalidPartyException e ) {
           throw new DialingException ("could not make a connection "
                + "because Call.connect caught InvalidPartyException\n\n" );
       } catch ( Exception e ) {
	   e.printStackTrace();
	   throw new DialingException (ME + "could not make a connection " 
		+ "because " + e.toString() );
       }
     else throw new DialingException (ME + "could not make a connection "
	+ " because there is no provider in service, and we couldn't "
	+ " make it come into service");
   }

   /**
    * hangup()
    * This method calls the disconnect() method of the Connection object
    * to end the call.  Also, it removes the callObserver 
    */
    private void hangup() {

        // drop my connection
        callInP = false; // set this now, else hangup() will be called again
	if (destConnection.getState() == Connection.ALERTING) {
	  // TBD make it stop ringing! ... but How??
        }
        try {
            System.out.print("\nDropping connections...\n\n");
            if ( myConnection != null ) 
                myConnection.disconnect();
	    if ( destConnection != null )
		destConnection.disconnect();
	    if ( call != null )
	       call.removeObserver( co );
        }
        catch (PrivilegeViolationException e) {
           System.out.print ( ME + 
	   "Connection.disconnect(): caught PrivilegeViolationException\n\n" );
        }
        catch (ResourceUnavailableException e) {
           System.out.print ( ME + 
	   "Connection.disconnect(): caught ResourceUnavailableException\n\n" );
        }
        catch (MethodNotSupportedException e) {
           System.out.print ( ME +
	   "Connection.disconnect(): caught MethodNotSupportedException\n\n" );
        }
        catch (InvalidStateException e) {
           System.out.print ( ME + 
	   "Connection.disconnect(): caught InvalidStateException\n\n" );
        }
        catch (PlatformException e) {
           System.out.print ( ME + 
	   "Connection.disconnect(): caught PlatformException\n\n" );
        }
    }

    /**
     * This method runs the thread - this thread continues to run until
     * the call is hungup, or the PBX Server is shut down, or placing 
     * the call failed (then we exit right away).
     */
    public void run() {
	if (LOG) System.out.println (ME + "is running");
        try{
            this.sleep(1);  // Why? just to catch InterruptedException
            makeCall(calling, called);
	    finishCall();
        } catch (DialingException e){
	    System.out.println (e);
	    if ( LOG ) System.out.println (ME + "If that was an " +
	       " invalid state exception, could be calling line is " +
	       " not off-hook (so call won't go through)");
	    // Send back a reject packet
	    sendRejectPacket ( myPacket );
	} 
        catch(InterruptedException ie){}
	if (LOG) System.out.println (ME + "end of thread " + this );
    }

    /**
     * This method returns true if the PBX has successfully placed
     * a call and this call is still in progress.
     * @return true if call is still in progress
     */

    public boolean callInProgress(){
        return callInP;
    }

    /**
     * This method returns true is the PBX is in the act of dialing
     * a destination telephone.  The call has not yet been established.
     * @return true is PBX is dialing a number
     */

    public boolean dialingInProgress() {
       return dialingInP;
    }

// =======================  Dialer private methods =====================
 /**
  * This method creates a CONFIRM packet and sends it back to the
  * appropriate gateway to acknowledge that the hangup is complete
  * March 15, 1999 - this routine might not be needed
  * @return void
  */
 private void sendConfirmPacket ( InvitePacket ip ) 
 throws PBXSignalingException {

   int mID; // method ID
   int pID; // packet ID

   if ( LOG ) System.out.println ( ME + " sending a CONFIRM packet " );
   if ( ip == null )
   throw new PBXSignalingException (ME + " sendConfirmPacket was asked "
       + "to confirm a null InvitePacket");
   
   mID = ip.getMethodID();
   pID = ip.getPacketID();

   // ignore all packets that aren't hangup requests
   if ( mID == SignalID.HANGUP ) {

       // create a CONFIRM Packet with Result type CONFIRMED
       SigPacket sp = new SigPacket (SignalID.CONFIRMED, SignalID.CONFIRM);

       // send it back to the gateway
       Location gw = ip.getSenderLoc();
       if ( LOG ) System.out.println ( ME + " sending CONFIRM to "
	  + "gw.toString(), whose IP address is " + gw.getIP() 
	  + " and port is " + gw.getPort() );
   }

   // dispose of the InvitePacket
   ip = null;

   }

 /**
  * This method is called when a busy tone was encountered
  * @param sp is the InvitePacket that originally caused us to ring this phone
  * @return void
  */

 private void sendBusyPacket (InvitePacket sp) {
    sp.busy();
    boolean b = sendPacket ( sp );
    if ( LOG ) {
       System.out.print (ME +  "Attempt to send busy packet ");
       if ( b ) System.out.println("succeeded");
       else System.out.println("failed");
    }
 }

 /**
  * This method sends an ACCEPT Result packet to the Gateway when 
  * the Dial is successfully initiated (i.e. telephone at other end 
  * is ringing) and then the phone was picked up within TIMEOUT msecs
  * @param ip The InvitePacket that we were using to dial with
  */
 private void sendAcceptPacket ( InvitePacket ip ) {
    ip.accept();
    boolean b = sendPacket ( ip );
    if ( LOG ) {
       System.out.print (ME +  "Attempt to send accept packet ");
       if ( b ) System.out.println("succeeded");
       else System.out.println("failed");
    }
 }

 /**
  * This method sends a REJECT Result packet when the Dial failed
  * @param ip The InvitePacket that we were using to dial with
  */
 private void sendRejectPacket ( InvitePacket ip ) {
    ip.reject("We don't know why the call was rejected; busy line?");
    boolean b = sendPacket ( ip );
    if ( LOG ) {
       System.out.print (ME +  "Attempt to send reject packet ");
       if ( b ) System.out.println("succeeded");
       else System.out.println("failed");
    }
 }

 /**
  * This method sends the packet back to the Gateway from which the
  * Invite packet had been received.  [WAS: where it came from.]
  * A new socket is created.  The packet's fields have already been 
  * appropriately adjusted.
  * @param sp The packet to be sent
  * @return true if packet was successfully sent, false otherwise
  */
  private boolean sendPacket ( InvitePacket sp ) {

     String ip;             // our Application and/or Gateway's name
     Location destAdd;      // Location of our Application
     int      p;            // The port where app and/or gateway is listening

     if ( LOG ) System.out.println ( ME + "sending a Packet <"
	+ sp + ">");
     if ( sp == null ) return false;

     InetAddress gateAdd = sendSock.getInetAddress();

     // Which Application is this packet originally from?
     if ( LOG ) {
        destAdd = sp.getSenderLoc();
        ip = destAdd.getIP();
        p = destAdd.getPort();
        System.out.println ( ME + "our application: ip = " + ip
	+ ", destAdd = " + destAdd.toString() + 
	", and the port is " + p );

        // Which Gateway sent this invite to us?
        System.out.println ( ME + "our Gateway: ip = " + gateAdd.getHostName()
	+ ", and the port is " + sendSock.getPort() );
     }

     // Write the packet out to the socket
     try {
        DataOutputStream f = new DataOutputStream ( sendSock.getOutputStream());
        ObjectOutputStream s = new ObjectOutputStream ( f );
        s.writeObject ( sp );
        s.flush();
	sendSock.close();
     } catch (IOException e) {
	// TBD throw ConnectFailedToOpenSocketException
	return false;
     }

     if ( LOG ) System.out.println ( ME + " sent the packet to " 
	+ gateAdd.getHostName() );
     return true;

  }

  /**
   * This method is called after a connection has been made and
   * ringing has started.  The goal now is to wait for a Connection.CONNECTED
   * state.  Check every second.
   * Connection.ALERTING probably means the phone is ringing
   * Connection.FAILED probably means the phone is busy
   * This routine sends an accept, busy, or reject, depending on circumstances
   * @return false if timeout, true if call picked up
   */

  private boolean callPickedUp () {
    int state;
    for ( int i = 0; i < TIMEOUT/1000; i++ ) {
      state = destConnection.getState();
      switch (state) {
         case Connection.CONNECTED:
	    return true;
	 case Connection.ALERTING:
	    try {
	       sleep (1000);
	    } catch ( InterruptedException e ) {}
	    break;
	 case Connection.FAILED:
            if (LOG) System.out.println (ME + "call got a busy signal.");
            dialingInP = false;
            sendBusyPacket ( myPacket );
	    hangup(); 
	    return false;
	 default:
	    if ( LOG ) {
	       System.out.print (ME + "in callPickedUP, " +
	          "unexpected Connection state is ");
	       switch (state) {
		  case Connection.IDLE:
		     System.out.println ("IDLE"); break;
		  case Connection.DISCONNECTED:
		     System.out.println ("DISCONNECTED"); break;
		  case Connection.INPROGRESS:
		     System.out.println ("INPROGRESS"); break;
		  case Connection.UNKNOWN:
		     System.out.println ("UNKNOWN"); break;
		  default:
		     System.out.println ("garbage");
	       }
	    }
      }
    }
    // Still ringing - send reject packet
    if (LOG) System.out.println (ME + "call timed out while ringing."
       + "  No Answer" );
    dialingInP = false;
    sendRejectPacket ( myPacket );
    hangup();
    return false;  
  } //                        ends callPickedUp

  /** 
   * This method is called when the phone has started rining.  We have
   * a call in progress.  We don't know whether it will be picked up or
   * if the ringing terminal is already busy.
   *
   * If the call is accepted, Dialer thread hangs here until the hangup
   * request comes in.  Otherwise we send a BUSY or REJECT and die now.
   */

  private void finishCall() {
     if ( WAIT ) 
	waitForPickup(); // could invoke acceptAndWait
     else
	acceptAndWait(); // blocks until resumed by request for hangup
  }

  /**
   * This method is called to wait for a ringing phone to be picked up
   * before sending an accept packet back to the gateway.
   * Upon entry to this method, dialingInP = false, callInP = false
   */

  private void waitForPickup() {
     if ( callPickedUp() ) {
        acceptAndWait();      // send accept packet now
     } else {                 // busy or reject already sent; commit suicide
          // invoke myPBX's method so that it can clean up Dialers
       myPBX.handleHangupInvite ( myPacket );
       if ( LOG ) System.out.println ( ME + "has hung up the "
	  + "call and this thread [" + this + "] dies.");
     }
  }

  /** 
   * This method is called to send an accept packet to the gateway
   * and then wait for the call to finish (be hung up).
   */

 private void acceptAndWait() {
    callInP = true; dialingInP = false;
    if (LOG) System.out.println (ME + "placed call successfully");
    sendAcceptPacket ( myPacket );
    this.suspend();// to be resumed by PBX Signaling
    hangup();//hanging the connection
 }

}
