package apps.conf;

import java.util.*;
import cnrg.itx.signal.*;
import cnrg.itx.signal.SignalEvent.*;
import cnrg.itx.datax.*;
import cnrg.itx.datax.devices.*;
import cnrg.itx.ds.*;
import javax.swing.event.*;

import apps.pint.AddrVerifyException;
import java.net.InetAddress;

	/**
	 * ConnectionManager maintains the actual connecting of participants to conferences.
	 * <p>
	 * Each ConnectionManager communicates with up to one participant. 
	 * ConnectionManagers may either dial a participant to invite him/her to the conference
	 * or wait for incoming calls to be patched into the conference. 
	 * <p>
	 * When a ConnectionManager connects to a participant, the participant's audio stream is
	 * patched into the parent Conference's mixer, and output from the mixer is
	 * sent to the participant.
	 * <p>
	 * To use ConnectionManager, first create one using one of the constructors, then activate
	 * it by calling the dial(uid, hearSelf) method, or the recieve(hearSelf) method.
	 * <p>
	 * By specifying the hearSelf boolean in either the dial of recieve methods, the conference
	 * can choose weather or not the participant can hear his/her own audio output in the conference. 
	 */

public class ConnectionManager extends Observable  implements SignalingObserver {
		
	/** is this connectionManager configured to recieve calls? */
	private boolean incoming = true;
	/** the conference object of our caller		 */
	private Conference conf = null;
	/** has the connection been opened yet?		 */
	private boolean open =false;
	/** was the dialed call accepted?		 */
	private boolean accepted = false;
	/** the number of instances of ConnectionManager 		 */
	private static int instances = 0;
	/**	this ConnectionManager's unique instance number 		 */
	private int instance;
	/** the MixerChannel that serves as the source of audio for the conference		 */
	private MixerChannel mixer;     
	/** the incoming data channel for this ConnectionManager		 */
	private Channel in;
	/** the outgoing data channel for this ConnectionManager		 */
	private Channel out;
	/**	the network destination through which data from this ConnectionManager is sent		 */
	private NetworkDestination netDest;
	/** the network source through which audio data is recieved from a participant		 */
	private NetworkSource netSource;
	/** can the participant connected to this ConnectionManager hear itself?		 */
	private boolean hearSelf = false;
	/**	the login uid of the conference	 */
	private String myUid = null;
	/** the login password of the conference		 */
	private String myPass = null;
	/** the description string for the conference		 */
	private String myTopic = null;
	/** the DesktopSignaling object for this ConnectionManager 		 */
	private DesktopSignaling ds;
	/** are we using directory services?	 */
	private boolean useDS = true;
	/** the SignalConnection this ConnectionManager uses to communicate with its participant		 */
	private SignalConnection connection = null;
	/** the list of locations where we can find the participant	 */
	private LocationList locations = null; 
	/** the list of DialManagers handling our simultaneous dials	 */
	private Vector calls = null;
	/** Who are we talking to?	 */
	private String peer = "Idle";
	
	/**
	 * Constructor builds a new ConnectionManager for managing communication with a conference participant.
	 * @param conf the parent conference object
	 */
	public ConnectionManager(Conference conf) {
		instance = instances++;
		this.conf = conf;
		this.mixer = conf.getMixer();
		this.useDS = conf.isUsingDS();
		conf.partsChanged();
		
	}
		
	/**
	 * Constructor builds a new ConnectionManager for managing communication with a conference participant.
	 * This constructor allows the specification of specific confrence name and login information.
	 * @param mixer the mixer to source audio for this connection
	 * @param MyUid the uid of the conference
	 * @param MyPass the password corresponding to MyUid
	 * @param MyTopic the topic for the conference
	 */
	public ConnectionManager(Conference conf,String ioMyUid,String ioMyPass, String ioMyTopic) {
		this(conf);
		this.myUid = ioMyUid;
		this.myPass = ioMyPass;
		this.myTopic = ioMyTopic;
	}
		
	/**
	 * Sets the ConnectionManager into incoming mode and starts it waiting for a call. 
	 * When used in this mode, 
	 * ConnectionManager will spawn off a new answering instance of itself when it becomes 
	 * busy dealing with an incoming call.
	 * @param mixer the mixer to source audio for this connection
	 * @param conf the Conference object creating this ConnectionManager
	 */
	public void recieve(boolean hearSelf){
		incoming = true;
		open = true;
		peer = "Waiting for call";
		this.hearSelf = hearSelf;
		
		
		try {
			if (useDS) {
				if (myUid == null || myPass == null)
					ds = new DirtyDesktopSignaling(this, "conf0", "conf0" );
				else if (myTopic == null) 
					ds = new DirtyDesktopSignaling(this, myUid, myPass);
				else  {
					ds = new DirtyDesktopSignaling(this, myUid, myPass, myTopic+"(listening)", null);
				}
			}else {//Not using directory Services?
			/*	try {
					ds = new DirtyDesktopSignaling(this,myUid,5060); //trys the default sip port
				} catch (Throwable t) {
					try {
						ds = new DirtyDesktopSignaling(this,myUid,5061); //trys the port after the default
					} catch (Throwable th) {
			*/			ds = new DirtyDesktopSignaling(this,myUid,0); //if anything goes wrong, let java pick the port.
			/*		}
				}
			*/	
			}
			
			peer = peer+"    (port "+ds.getPort()+")"; //stick listening port at end of name
			conf.partsChanged();
			System.out.println("instance "+instance+" ready to answer");
		}catch (DirectoryServiceException dse) {
			dse.printStackTrace();
			this.close();											   
		}
	}
			 
	/**
	 * Handles the dialing of a participant for making outgoing connections. 
	 * @param uid The userID of the participant to dial
	 * @exception DirectoryServiceException thrown when lookup of uid fails
	 */
	public void dial(String uid, boolean hearSelf) throws DirectoryServiceException {
		String noDirectoryMsg = "No Directory";
		
		incoming = false;
		open = true;
		this.hearSelf = hearSelf;
		peer = "Dialing "+uid;
		conf.partsChanged();
		calls = new Vector();
		try {
			if (!useDS) { throw new DirectoryServiceException(noDirectoryMsg);
			}	//using directory services:
			if (myUid == null || myPass == null){
				ds = new DirtyDesktopSignaling(this, "conf0", "conf0" );
			}else if (myTopic == null) {
				ds = new DirtyDesktopSignaling(this, myUid, myPass);
			}else {
				ds = new DirtyDesktopSignaling(this, myUid, myPass, myTopic+"(dial-"+uid+")", null);
			}
			locations = ds.getLocationList(uid);
			if (locations != null) {
				if (locations.count() > 0) {
					Location l = locations.first();
					
					
					do{
						if (l.isDialable()) {
							calls.addElement(new DialManager(this,uid,l));
						}//TODO: add support for gateway when not dialable
						else {
							//the number is outside the net (conference hates the gateway so this isn't a priority...)
						}
					}while ((l=locations.next()) != null);
					
					
					
				} else 	throw new DirectoryServiceException("no listing for "+uid); 	
			} 	
			if (calls.isEmpty() && accepted == false) {
				throw new DirectoryServiceException("no accessable locations for "+uid);
			}
		} catch (DirectoryServiceException dse) {
			//no luck dialing thru directory. Try uising uid as an internet address:
			try {
				ds = new DirtyDesktopSignaling(this,myUid,0);
				
				uid = verifyAddr(uid);
				
				Location l = new Location("I",uid,uid);
				
				calls.addElement(new DialManager(this,uid,l));
				
			} catch (AddrVerifyException ave) {
				if (dse.getMessage().equals(noDirectoryMsg)) {
					throw new DirectoryServiceException(ave.getMessage());
				} else throw dse;
			}

			
		}	
		System.out.println("\tdialed");
	}
		
	/**
	 * builds the connection to be used for communication.
	 * The NetworkDestination is hidden in a DummyChannel so when signaling
	 * closes the DummyChannel, it doesn't close the mixer (the real source
	 * for the NetworkDestination.
	 */
	private void populateInOut() {
		try {
			netDest = new NetworkDestination(false); //don't use FEC and RTP: too much overhead
							
			// hook the mixer to feed into the net destination, 
			// get a channel from the mixer (in) to feed data into
			if (hearSelf) {
				mixer.addDestination(netDest);
				in = mixer.getNewInput();
			}else {
				in = mixer.getNewSubtractedInput(netDest);
			}	
			
			//Get a net source w/o FEC or RTP
			netSource = new NetworkSource(in,SpeakerDestination.SAMPLE_SIZE, false);
			
			//set the net source to feed into the mixer's input
			in.setSource(netSource);

			//We only show signaling this DummyChannel so it can configure
			//its destination (netDest) without hurting netDest's real source: the mixer
			
			//The DummyChannel conveys all property, open, and close messages like
			//a real channel, but does not have a thread to transport data.
			out =new DummyChannel();
			out.addDestination(netDest);
			//Keep everyone happy by giving out fake channel a fake source.
			out.setSource(new DummySource());
				
				
		} catch (DataException de) {
			de.printStackTrace();
		}
	}
		
			public void close() {
		close(connection);
	}
		
	/**
	 * Shuts down the given SignalConnection that was set up 
	 * by a ConnectionManager and removes it from the conference's listing
	 */
	public void close(SignalConnection sc) {
		if (incoming == false && calls != null) {
			//if we are still dialing, cancel all of the dialers.
			while (!calls.isEmpty()) {
				((DialManager)calls.firstElement()).close();
			}
			
		}
		
		
		if (open == false) {
			return;
		}
		if (mixer != null) {
			if (netDest!=null) {
				mixer.removeDestination(netDest); //remove the participant from the mixer's output
			}
			if (in != null) {
				mixer.removeInput(in); //closes "in" and "netSource";
			}
		}
			
		if (netSource != null)
			netSource.close();
			
				

		if (ds != null) {
			try {
				if (sc != null) {
					ds.Hangup(sc);
					System.out.println("Hangup Succeeded");
				}
			} catch (ConnectException e) {
			}

			ds.logout();
			System.out.println("logout complete");
		}
		open = false;
		conf.remove(this);
		conf.partsChanged();

	
			
	}
		
	/**
	 * Returns the connection being managed by this ConnectionManager
	 */
	public SignalConnection getConnection() {
		return connection;
	}
		
	public Conference getConference() {
		return conf;
	}
	
	public boolean getHearSelf() {
		return hearSelf;
	}
	
	public DesktopSignaling getDesktopSignaling() {
		return ds;
	}
	
	/**
	 * Check an net address string and return a valid, resolved version of it.
	 */
	private String verifyAddr(String in) throws AddrVerifyException{
		try {
			StringTokenizer tokens = new StringTokenizer(in,":");
			if (tokens.countTokens() == 1){
				throw new AddrVerifyException("Specify a port using the ':' character");
			}
			String host = tokens.nextToken();
			
			InetAddress ia = InetAddress.getByName(host);
			
			String port = tokens.nextToken();
			
			int portnum = Integer.parseInt(port);
			
			return ia.getHostAddress()+":"+portnum;
			
			
			
		}catch (Exception e) {
			throw new AddrVerifyException("Not a valid internet address");
		}
	}
	
	/**
	 * Called by DialManagers when they get an answer.The first DialManager
	 * to call this method is accepted, all others are told to abort.
	 * @return boolean true if this was the first one, false if not.
	 */
	public synchronized boolean accept(DialSignalEvent dse, DialManager dm) {
		if (accepted) {
			return false;
		}
		try {
				connection = dse.getSignalConnection();
				connection.open();
				//((AudioConnection)connection.getConnection()).addObserver(this);
				System.out.println("Connection accepted");
				accepted = true;
				open = true;
				this.netDest = dm.netDest;
				this.netSource = dm.netSource;
				this.in = dm.in;
				this.out = dm.out;
				
				peer = peer.substring(8,peer.length()); //removes the "Dialing " prefix from a user's listing
				conf.partsChanged();
				//when one call is picked up, we abort all other attempts
				for (Enumeration e = calls.elements();e.hasMoreElements();) {
					DialManager rdm = (DialManager)e.nextElement();
					if (!dm.equals(rdm)) {
						removeDialManager(rdm);
					}
				}
								
			} catch (DataException de) {
				de.printStackTrace();
			
			}		
		return true;
	}
	
	/**
	 * When a dialManager is done dialing, it tells the ConnectionManager to remove it from
	 * the list of pending dials.
	 */
	public void removeDialManager(DialManager d) {
		calls.removeElement(d);
		if (calls.isEmpty()) {
			this.close();
		}
	}
	
	/* ***** Observer interface support ******/
		
	/**
	 * prints the observable object
	 */
	public void update(Observable obs,Object obj){
		System.out.println(obj);
	}

	
		
		
	/* *********** ConnectionObserver Interface support ************/
		
		
	/** Called when an invitation is recieved. Called through the SignalingObserver interface.
	 * When an invitation is recieved, onInvite spawns off a new acceptor, then builds and opens 
	 * an AudioConnection to the participant.
	 */
	public void onInvite(InviteSignalEvent ise) {
		if(incoming && !accepted){
			accepted = true;
			conf.addAcceptor(); // spawn off a new acceptor to handle incoming calls while we're busy
			populateInOut();
			Connection c = new AudioConnection(in,out); 
			ise.accept(c);
			peer = ise.getSenderID().toString();
			conf.partsChanged();
		} else {
			ise.busy();
		}

	}
		
	/**
	 * When call is aborted, this is called, closing the connection and removing it from the conference.
	 */
	public void onAbortCall(AbortSignalEvent ase) {
		close(ase.getSignalConnection());
	}
		
	/**
	 * Called when call setup is complete: opens the connection.
	 */
	public void onStartCall(SignalConnection sc) {
		try {
			sc.open();
			connection = sc;
			conf.partsChanged();
		} catch (DataException de) {
			de.printStackTrace();
		}
			
	}
	

	
	/**
	 * closes connection
	 */	
	public void onHangup(HangupSignalEvent hse) {
			
		System.out.println("hanging up");
		SignalConnection conn = hse.getSignalConnection();
		close(conn);
			
	}
	public String toString() {
		if (!open) {
			return "Waiting for call";
		} else return peer;
	}
	
	
	public void onDTMF(DTMFSignalEvent dse) {
		//ds.sendDTMF(dse.getDTMF(),);
	}
	
		
	public void makeDirty() {
		conf.partsChanged();
	}
	
	public boolean isWaitingForCall() {
		return (incoming && open && !accepted);
	}
		

		
}