/*****************************************************************************
 *                                                                           *
 *         Licensed Materials - Property of IBM                              *
 *                                                                           *
 *         "Restricted Materials of IBM"                                     *
 *                                                                           *
 *         5799-QRY                                                          *
 *                                                                           *
 *         (C) Copyright IBM Corp. 1998                                      *
 *                                                                           *
 *****************************************************************************/
package com.ibm.devtools.SIPNoTE;

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

/**
 * This class represents the client side daemon which handles all
 * messaging interactions between the client processes and servers.</p>
 * 
 * Currently, it is responsible for:
 * <ol>
 * <li>Performing the initial user logon to the server.
 * <li>Receiving outgoing jephyr requests and repackaging them as SIP
 *     messages to be sent to the server.
 * <li>Receiving incoming SIP messages, processing their contents, and
 *     invoking the proper message handler (currently a popup window for
 *     Jephyr messages.)
 * <li>Spawning the SNWatch window for user interactions.
 * </ol></p>
 * 
 * The client uses a properties file (the .sipnote file) to change
 * a set of variables, such as server name, port, etc. from their
 * default values.
 *
 * @author Bill Nagy
 * @version 1.0
 */
public class SNClient
{
  static Properties props = new Properties();
  static SIPMCastIn mcastthread;
  static InetAddress localhost;
  static String localhostaddr;
  static String localhostname;
  static int localport = -1;
  static String servername;
  static int serverport;
  static boolean useipaddr = false; /*Should we use the IP address instead
				      of the name?  This is needed in
				      case the user does not have a fixed
				      IP address, and the name of the machine
				      is not stored in a DNS like entity.*/
  /**
   * Performs the initial user logon to the server.
   *
   * @param userid The name to register the user as.
   */
  private static boolean register(String userid)
  {
    SIPMessage rmsg = new SIPMessage();
    SIPMessage rspmsg = new SIPMessage();
    SIPurl requesturi = new SIPurl();
    ViaEntity via = new ViaEntity();
    ToEntity to = new ToEntity();

    requesturi.setHost(servername);    
    requesturi.setPort(serverport);    
    requesturi.setUser(userid);

    rmsg.setMessageType(rmsg.REQUEST);
    rmsg.setMethod(rmsg.REGISTER);

    rmsg.setRequestURI(new SIPurl[] {requesturi});
    
    if (useipaddr)
      via.setHost(localhostaddr);
    else
      via.setHost(localhostname);
    via.setPort(localport);

    to.setURL(requesturi);

    try
      {
	rmsg.addVia(via);
	rmsg.addTo(new ToEntity[] {to});
      }
    catch (MalformedMessageException e)
      {
	e.printStackTrace(System.err);
	return false;
      }	

    /*Add subscription information*/
    try
      {
	BufferedReader br = new BufferedReader(new FileReader(".snsubs"));
	String subline;

	while ((subline = br.readLine()) != null)
	  {
	    if (subline.length() != 0)
	      {
		/*Ignore #*/
		if ((subline.trim()).startsWith("#"))
		  continue;
		try
		  {
		    /*Strip leading ;*/
		    if (subline.charAt(0) == ';')
		      subline = subline.substring(1);
		    rmsg.addSubscription(subline);
		  }
		catch (StringIndexOutOfBoundsException e)
		  {
		  }
	      }
	  }

	br.close();
      }
    catch (FileNotFoundException e)
      {
      }
    catch (IOException e)
      {
	System.err.println("Error reading .snsubs file. --> "+e);
      }
    catch (MalformedMessageException e)
      {
	e.printStackTrace(System.err);
	return false;
      }

    try
      {
	Socket ssock = new Socket(servername, serverport);
	rmsg.writeMessage(ssock.getOutputStream());
	rspmsg.readMessage(ssock.getInputStream());
      }
    catch (UnknownHostException e)
      {
	e.printStackTrace(System.err);
	return false;
      }
    catch (IOException e)
      {
	e.printStackTrace(System.err);
	return false;
      }
    catch (MalformedMessageException e)
      {
	e.printStackTrace(System.err);
	return false;
      }

    if (rspmsg.getMessageType() != rspmsg.RESPONSE)
      {
	System.err.println("Failed to receive response message.");
	return false;
      }
    
    if (rspmsg.getStatusCode() != 200)
      {
	System.err.println("Error Registering to Server: "+rspmsg.getStatusCode());
	System.err.println("Response: "+rspmsg.getReasonPhrase());
	return false;
      }
    
    return true;
  }

  /**
   * Terminate the SNClient.  This will kill the spawned threads,
   * and perform any other cleanup which is necessary, including
   * unregistering the user.
   */
  public static void exit()
  {
    SIPMessage rmsg = new SIPMessage();
    SIPMessage rspmsg = new SIPMessage();
    SIPurl requesturi = new SIPurl();
    ViaEntity via = new ViaEntity();
    ToEntity to = new ToEntity();
    
    /*Halt the multicast receiver*/
    mcastthread.stop();

    requesturi.setHost(servername);    
    requesturi.setPort(serverport);    
    requesturi.setUser(System.getProperty("user.name"));
    
    rmsg.setMessageType(rmsg.REQUEST);
    rmsg.setMethod(rmsg.UNREGISTER);

    rmsg.setRequestURI(new SIPurl[] {requesturi});
    
    if (useipaddr)
      via.setHost(localhostaddr);
    else
      via.setHost(localhostname);
    via.setPort(localport);

    to.setURL(requesturi);

    try
      {
	rmsg.addVia(via);
	rmsg.addTo(new ToEntity[] {to});
      }
    catch (MalformedMessageException e)
      {
	e.printStackTrace(System.err);
	System.exit(0);
      }	

    try
      {
	Socket ssock = new Socket(servername, serverport);
	rmsg.writeMessage(ssock.getOutputStream());
	rspmsg.readMessage(ssock.getInputStream());
      }
    catch (UnknownHostException e)
      {
	e.printStackTrace(System.err);
	System.exit(0);
      }
    catch (IOException e)
      {
	e.printStackTrace(System.err);
	System.exit(0);
      }
    catch (MalformedMessageException e)
      {
	e.printStackTrace(System.err);
	System.exit(0);
      }

    if (rspmsg.getMessageType() != rspmsg.RESPONSE)
      {
	System.err.println("Failed to receive response message.");
	System.exit(0);
      }
    
    if (rspmsg.getStatusCode() != 200)
      {
	System.err.println("Error UnRegistering: "+rspmsg.getStatusCode());
	System.err.println("Response: "+rspmsg.getReasonPhrase());
	System.exit(0);
      }
    
    System.exit(0);
  }

  /**
   * This is the main routine (surprise, surprise).  It currently
   * takes two types of command line arguments.  The first,
   * "-p <port>", specifies which port the client will listen on for
   * incoming SIP messages.  The second, "-i" indicates that the client
   * should use its IP address, and not hostname, when filling in the
   * message headers.  This is very useful in cases where the client
   * does not have a valid DNS entry for the server to do name resolution
   * upon.
   */
  public static void main(String argv[])
  {
    FileInputStream fin;
    int cnt;
    boolean argok = true;

    try
      {
	fin = new FileInputStream(".sipnote");
	props.load(fin);
      }
    catch (FileNotFoundException e)
      {
      }
    catch (IOException e)
      {
	System.err.println("I/O Error loading .sipnote file --> "+e);
	return;
      }

    /*Process commandline args*/
    for (cnt = 0; cnt < argv.length; cnt++)
      {
	if (argv[cnt].equalsIgnoreCase("-p"))
	  {
	    if (++cnt < argv.length)
	      {
		try
		  {
		    localport = (new Integer(argv[cnt])).intValue();
		  }
		catch (NumberFormatException e)
		  {
		    System.err.println("Bad port number for -p");
		    argok = false;
		  }
	      }
	    else
	      {
		System.err.println("Missing port number for -p");
		argok = false;
	      }
	  }
	else
	  if (argv[cnt].equalsIgnoreCase("-i"))
	    {
	      useipaddr = true;
	    }
	  else
	    argok = false;

	if (!argok)
	  {
	    System.out.println("Usage: SNClient [-p <port>] [-i]");
	    System.out.println("  -p: Port number for SIP messages");
	    System.out.println("  -i: Use IP address instead of hostname");
	    return;
	  }
      }

    if (localport == -1)
      {
	localport = (new Integer(props.getProperty("SNClient.sport",
						   "5061")).intValue());
      }
    
    try
      {
	localhost = InetAddress.getLocalHost();
	localhostaddr = localhost.getHostAddress();
	localhostname = localhost.getHostName();
      }
    catch (UnknownHostException e)
      {
	e.printStackTrace(System.err);
	return;
      }

    /*Try to register*/
    servername = props.getProperty("SNServer.name", "localhost");
    serverport = (new Integer(props.getProperty("SNServer.port",
						"5060")).intValue());

    if (register(System.getProperty("user.name")))
      {
	//We should really load the handler modules here, but
	//for now we will hard code 2 of them.  One to handle
	//messages that we want to write, and the other
	//to handle incoming messages.

	new JOutgoing().start();

	mcastthread = new SIPMCastIn();
	mcastthread.start();
	
	try
	  {
	    ServerSocket ss = new ServerSocket(localport);
	    System.out.println("SIPNoTEClient started on port "+localport+".");
	    while(true)
	      new ClientConnection(ss.accept()).start();
	  }
	catch (IOException e)
	  {
	    e.printStackTrace(System.err);
	  }
      }
  }
}

/**
 * This class represents the entity which handles incoming SIP messages
 * and does the appropriate processing on them to extract the Jephyr message.
 */
class ClientConnection extends Thread
{
  Socket client;

  ClientConnection(Socket client) throws SocketException
  {
    this.client = client;
    setPriority(NORM_PRIORITY-1);
  }

  public void run()
  {
    try
      {
	InputStream in;
	OutputStream out;
	SIPMessage message = new SIPMessage();

	try
	  {
	    in = client.getInputStream();
	    out = client.getOutputStream();
	  }
	catch (IOException e)
	  {
	    e.printStackTrace(System.err);
	    return;
	  }

	message.readMessage(in);
	
	switch(message.getMethod())
	  {
	  case message.MESSAGE:
	    SendResponseMsg(out, message.MESSAGE, 200, "Message Received");
	    JephyrSIPTransform transform = new JephyrSIPTransform(message);
	    JephyrWnd msgwnd = new JephyrWnd(transform.SIPToJephyr());
	    break;
	  default:
	    System.err.println("Unsupported message type, ignoring.");
	  }

	if (DebugDefine.DEBUG)
	  System.out.println("Terminating connection.");
	try
	  {
	    client.close();
	  }
	catch (IOException e)
	  {
	    e.printStackTrace(System.err);
	  }
      }
    catch (MalformedMessageException e)
      {
	e.printStackTrace(System.err);
	if (DebugDefine.DEBUG)
	  System.out.println("Terminating connection.");
	try
	  {
	    client.close();
	  }
	catch (IOException ie)
	  {
	    ie.printStackTrace(System.err);
	  }
      }
  }

  private void SendResponseMsg(OutputStream out, int responseto,
			       int responsecode, String reasonphrase)
  {
    SIPMessage rspmsg = new SIPMessage();
    ViaEntity via = new ViaEntity();
    
    rspmsg.setMessageType(rspmsg.RESPONSE);
    rspmsg.setMethod(responseto);
    
    rspmsg.setStatusCode(responsecode);
    rspmsg.setReasonPhrase(reasonphrase);
    
    if (SNClient.useipaddr)
      via.setHost(SNClient.localhostaddr);
    else
      via.setHost(SNClient.localhostname);
    via.setPort(SNClient.localport);
    
    try
      {
	rspmsg.addVia(via);
      }
    catch (MalformedMessageException e)
      {
	System.err.println(e);
	return;
      }
    
    try
      {
	rspmsg.writeMessage(out);
      }
    catch (MalformedMessageException e)
      {
	System.err.println(e);
	return;
      }
    
  }
}

/**
 * This class represents the entity which accepts outgoing Jephyr
 * requests for further processing.
 */
class JOutgoing extends Thread
{
  public void run()
  {
    int localport;
    try
      {
	localport =
	  (new Integer(SNClient.props.getProperty("SNClient.jport",
						 "5062")).intValue());

	ServerSocket ss = new ServerSocket(localport);
	System.out.println("Outgoing box started on port "+localport+".");
	while(true)
	  new OutgoingMsgConnection(ss.accept()).start();
      }
    catch (IOException e)
      {
	e.printStackTrace(System.err);
      }
  }
}

/**
 * This class represents the entity which handles outgoing Jephyr requests
 * by processing the request, and sending out a corresponding SIP message.
 */
class OutgoingMsgConnection extends Thread
{
  Socket client;

  OutgoingMsgConnection(Socket client) throws SocketException
  {
    this.client = client;
    setPriority(NORM_PRIORITY-1);
  }

  public void run()
  {
    try
      {
	ObjectInputStream in =
	  new ObjectInputStream(client.getInputStream());
	ObjectOutputStream out =
	  new ObjectOutputStream(client.getOutputStream());
	
	Object incoming = in.readObject();

	if (incoming instanceof JMessage)
	  {
	    JephyrSIPTransform transform =
	      new JephyrSIPTransform((JMessage)incoming);

	    SIPMessage newmsg = transform.JephyrToSIP(SNClient.localhostname,
						      SNClient.localport,
						      SNClient.servername,
						      SNClient.serverport);
	    SIPMessage rspmsg = new SIPMessage();

	    //What do we do here if we can't send the message
	    try
	      {
		Socket ssock =
		  new Socket(SNClient.servername, SNClient.serverport);
		newmsg.writeMessage(ssock.getOutputStream());
		rspmsg.readMessage(ssock.getInputStream());
	      }
	    catch (UnknownHostException e)
	      {
		e.printStackTrace(System.err);
	      }
	    catch (IOException e)
	      {
		e.printStackTrace(System.err);
	      }
	    catch (MalformedMessageException e)
	      {
		e.printStackTrace(System.err);
	      }
	    
	    if (rspmsg.getMessageType() != rspmsg.RESPONSE)
	      {
		System.err.println("Failed to receive response message.");
	      }

	    if (rspmsg.getStatusCode() != 200)
	      {
		System.err.println("Error Sending Message to Server: "+rspmsg.getStatusCode());
		System.err.println("Response: "+rspmsg.getReasonPhrase());
	      }

	    /*If this was a locate message, we need to process the response*/
	    if (newmsg.getMethod() == newmsg.LOCATE)
	      {
		SIPurl[] locrsp;
		int cnt;

		try
		  {
		    locrsp = rspmsg.getLocation();
		    
		    String[] locstrs = new String[locrsp.length];

		    for (cnt = 0; cnt < locrsp.length; cnt++)
		      {
			try
			  {
			    locstrs[cnt] = locrsp[cnt].toURLString();
			    if (DebugDefine.DEBUG)
			      System.out.println(locstrs[cnt]);
			  }
			catch (MalformedSIPurlException e)
			  {
			    //We should never get here
			  }
		      }
		    
		    JMessage jresp = new JMessage(locstrs, "OK");
		    out.writeObject(jresp);

		  }
		catch (MalformedMessageException e1)
		  {
		    System.err.println(e1);
		  }
	      }
	  }
	client.close();
      }
    catch (EOFException e3) //Normal EOF
      {
	try
	  {
	    client.close();
	  }
	catch (IOException e) {}
      }
    catch (IOException e)
      {
	e.printStackTrace(System.err);
      }
    catch (ClassNotFoundException e2)
      {
	e2.printStackTrace(System.err);
      }
  }
}

/**
 * This class represents the entity which handles SIP messages which
 * have been multicast.  Currently, the only type of these messages
 * which should appear are logon/logoff notifications sent by the
 * SNServer.
 */
class SIPMCastIn extends Thread
{
  public void run()
  {
    InetAddress mcastgroup;
    int mcastport;
    MulticastSocket s;

    try
      {
	SNWatch watcher = new SNWatch();
	watcher.poll();
	mcastgroup = InetAddress.getByName(SNClient.props.getProperty("SNClient.maddr", "228.4.9.12"));
	mcastport =
	  (new Integer(SNClient.props.getProperty("SNClient.mport",
						 "5063")).intValue());
	s = new MulticastSocket(mcastport);
	s.joinGroup(mcastgroup);
	
	while (true)
	  {
	    byte[] buf = new byte[4000]; /*This should be more than enough*/
	    DatagramPacket recv = new DatagramPacket(buf, buf.length);

	    s.receive(recv);
	    
	    if (DebugDefine.DEBUG)
	      System.out.println("Got mcast packet of length: "+recv.getLength());
	    ByteArrayInputStream bais =
	      new ByteArrayInputStream(recv.getData(), 0, recv.getLength());

	    try
	      {
		SIPMessage message = new SIPMessage();

		message.readMessage(bais);
		
		switch(message.getMethod())
		  {
		  case message.MESSAGE:
		    JephyrSIPTransform transform =
		      new JephyrSIPTransform(message);
		    JMessage inmsg = transform.SIPToJephyr();

		    String[] msglines = inmsg.getMessage();
		    if (msglines[0].indexOf("logged onto") != -1)
		      {
			watcher.addAlias(msglines[0].substring(0, msglines[0].indexOf(" ")));
		      }
		    if (msglines[0].indexOf("logged out of") != -1)
		      {
			watcher.removeAlias(msglines[0].substring(0, msglines[0].indexOf(" ")));
		      }

		    //JephyrWnd msgwnd = new JephyrWnd(transform.SIPToJephyr());
		    break;
		  default:
		    System.err.println("Unsupported message type, ignoring.");
		  }
		
		try
		  {
		    bais.close();
		  }
		catch (IOException e)
		  {
		    e.printStackTrace(System.err);
		  }
	      }
	    catch (MalformedMessageException e)
	      {
		e.printStackTrace(System.err);
		try
		  {
		    bais.close();
		  }
		catch (IOException ie)
		  {
		    ie.printStackTrace(System.err);
		  }
	      }
	  }
      }
    catch (IOException e)
      {
	e.printStackTrace(System.err);
      }
    catch (Exception e) //Fix this in Watcher
      {
	e.printStackTrace(System.err);
      }
  }
}


