/*****************************************************************************
 *                                                                           *
 *         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 server side daemon which handles directory
 * services for the SIPNoTE implementation, as well as message routing
 * functions.
 * <p>
 * The server uses a properties file (the .sipnote file) to change
 * a set of variables, such as server port, multicast address, etc.
 * from their default values.
 *
 * @author Bill Nagy
 */
public class SNServer
{
  static Hashtable users = new Hashtable();
  static int localport = -1;
  static String localhostname;
  static String localhostaddr;
  static Properties props = new Properties();

  /**
   * This is the main routine (surprise, surprise.)  It currently takes
   * only one command line argument, a "-p" which specifies the port on
   * which it will listen for incoming messages.  
   */
  public static void main(String argv[])
  {
    Integer portint;
    FileInputStream fin;
    boolean argok = true;
    int cnt;

    /*Try to read startup values*/
    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
	  argok = false;

	if (!argok)
	  {
	    System.out.println("Usage: SNServer [-p <port>]");
	    System.out.println("  -p: Port number for SIP messages");
	    return;
	  }
      }

    if (localport == -1)
      {
	localport = (new Integer(props.getProperty("SNServer.port",
						   "5060")).intValue());
      }

    try
      {
	InetAddress localhost = InetAddress.getLocalHost();
	localhostaddr = localhost.getHostAddress();
	localhostname = localhost.getHostName();
      }
    catch (UnknownHostException e)
      {
	System.err.println(e);
	return;
      }

    try
      {
	ServerSocket ss = new ServerSocket(localport);
	System.out.println("SIPNoTEServer started on port "+localport+".");
	while(true)
	  new ServerConnection(ss.accept()).start();
      }
    catch (IOException e)
      {
	System.err.println("I/O error: "+e);
      }
  }

}

/**
 * This class represents the entity which handles incoming SIP messages
 * and does the appropriate processing on them, depending upon what
 * type of message it is.
 */
class ServerConnection extends Thread
{
  Socket client;

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

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

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

	message.readMessage(in);
	
	//we need to check messagetype here, but for right
	//now only assume that we get requests

	SIPurl userURIs[];
	ViaEntity userloc;
	String userid;
	UserRecord recipient;
	SIPMessage rspmsg = new SIPMessage();
	HostRecord newhost;

	switch(message.getMethod())
	  {
	  case message.REGISTER:
	    {
	      /*Get the user name (pg 24)*/
	      userURIs = message.getRequestURI();
	      userid = userURIs[0].getUser();
	      
	      /*Change this to lowercase to facilitate mapping*/
	      //Do we really want to do this?
	      userid = userid.toLowerCase();
	      
	      /*Get the host/port we should pass the message to (pg 24)*/
	      try
		{
		  userloc = message.getOriginatorVia();
		}
	      catch (MalformedHeaderException e)
		{
		  /*If the via header of the originator is wrong, there
		    isn't much we can do*/
		  SendResponseMsg(out, message.REGISTER, 400, e.toString());
		  System.err.println(e);
		  if (DebugDefine.DEBUG)
		    System.out.println("Terminating connection.");
		  try
		    {
		      client.close();
		    }
		  catch (IOException ie)
		    {
		      System.err.println("I/O Error --> " + ie);
		      return;
		    }
		  return;
		}
	      
	      newhost = new HostRecord(userloc.getHost(),
				       userloc.getPort());
	      
	      /*See if user is already in hashtable*/
	      if (SNServer.users.containsKey(userid))
		{
		  /*If they are, add a new location for them.*/
		  /*Users may want to register to receive their messages
		    at more than one location.*/
		  UserRecord olduser = (UserRecord)SNServer.users.get(userid);
		  olduser.addLocation(newhost);
		  olduser.addSubscriptions(message.getSubscriptions());
		}
	      else
		{
		  try
		    {
		      /*If not, add the user*/
		      UserRecord newuser = new UserRecord(userid, newhost);
		      SNServer.users.put(userid, newuser);
		      newuser.addSubscriptions(message.getSubscriptions());
		    }
		  catch (NullPointerException e)
		    {
		      //What do we want to do here?
		      System.err.println(e);
		      break;
		    }
		}
	      SendResponseMsg(out, message.REGISTER, 200, "Register Succeeded");
	      
	      /*Broadcast logon message*/
	      JMessage jnmessage =
		new JMessage(new String[] {userid+" logged onto "+userloc.getHost()}, "SIPNoTE Server", new String[] {";SNNotification"});
	      
	      JephyrSIPTransform transform =
		new JephyrSIPTransform(jnmessage);
	      
	      SIPMessage newmsg = transform.JephyrToSIP(SNServer.localhostname,
							SNServer.localport,
							SNServer.localhostname,
							SNServer.localport);
	      ByteArrayOutputStream baos = new ByteArrayOutputStream();
	      newmsg.writeMessage(baos);
	      
	      try
		{
		  InetAddress mcastgroup = InetAddress.getByName("228.4.9.12");
		  MulticastSocket ms = new MulticastSocket(5063);
		  byte[] ba = baos.toByteArray();
		  DatagramPacket dp = new DatagramPacket(ba, ba.length,
							 mcastgroup, 5063);
		  ms.send(dp, (byte)1);
		  
		  ms.close();
		}
	      catch (UnknownHostException e)
		{
		  e.printStackTrace(System.err);
		}
	      catch (IOException e)
		{
		  e.printStackTrace(System.err);
		}
	    }
	    break;
	  case message.UNREGISTER:
	    {
	      //Remove a user from a particular host
	      //Probably also want to remove all instances.
	      
	      /*Get the user name (pg 24)*/
	      userURIs = message.getRequestURI();
	      userid = userURIs[0].getUser();
	      
	      /*Change this to lowercase to facilitate mapping*/
	      //Do we really want to do this?
	      userid = userid.toLowerCase();
	      
	      /*Get the host/port we should pass the message to (pg 24)*/
	      try
		{
		  userloc = message.getOriginatorVia();
		}
	      catch (MalformedHeaderException e)
		{
		  /*If the via header of the originator is wrong, there
		    isn't much we can do*/
		  SendResponseMsg(out, message.REGISTER, 400, e.toString());
		  System.err.println(e);
		  if (DebugDefine.DEBUG)
		    System.out.println("Terminating connection.");
		  try
		    {
		      client.close();
		    }
		  catch (IOException ie)
		    {
		      System.err.println("I/O Error --> " + ie);
		      return;
		    }
		  return;
		}
	      
	      newhost = new HostRecord(userloc.getHost(),
				       userloc.getPort());
	      
	      /*See if user is already in hashtable*/
	      if (SNServer.users.containsKey(userid))
		{
		  /*If they are, remove the old location for them*/
		  /*If they have no other locations, consider the user
		    completely logged out, and remove the entry from the
		    hash table.*/
		  UserRecord olduser = (UserRecord)SNServer.users.get(userid);
		  if ((olduser.removeLocation(newhost)) == 0)
		    {
		      if (DebugDefine.DEBUG)
			System.out.println(userid+" has no more registered locations.  Removing.");
		      SNServer.users.remove(userid);
		    }
		}
	      /*Return an OK if the user hasn't registered previously(pg24)*/
	      
	      SendResponseMsg(out, message.UNREGISTER, 200,
			      "Unregister Succeeded");
	      
	      /*Broadcast logoff message*/
	      JMessage jnmessage =
		new JMessage(new String[] {userid+" logged out of "+userloc.getHost()}, "SIPNoTE Server", new String[] {";SNNotification"});
	      
	      JephyrSIPTransform transform =
		new JephyrSIPTransform(jnmessage);
	      
	      SIPMessage newmsg = transform.JephyrToSIP(SNServer.localhostname,
							SNServer.localport,
							SNServer.localhostname,
							SNServer.localport);
	      ByteArrayOutputStream baos = new ByteArrayOutputStream();
	      newmsg.writeMessage(baos);
	      
	      try
		{
		  InetAddress mcastgroup = InetAddress.getByName("228.4.9.12");
		  MulticastSocket ms = new MulticastSocket(5063);
		  byte[] ba = baos.toByteArray();
		  DatagramPacket dp = new DatagramPacket(ba, ba.length,
							 mcastgroup, 5063);
		  ms.send(dp, (byte)1);
		  
		  ms.close();
		}
	      catch (UnknownHostException e)
		{
		  e.printStackTrace(System.err);
		}
	      catch (IOException e)
		{
		  e.printStackTrace(System.err);
		}
	    }
	    break;
	  case message.MESSAGE:
	    /*Get the user name (pg 24)*/
	    userURIs = message.getRequestURI();

	    /*See if we can find all of the users - If so, return message
	      received.  If not, complain*/
	    String missingusers = new String();
	    String tempuser;
	    for (cnt = 0; cnt < userURIs.length; cnt++)
	      {
		userid = userURIs[cnt].getUser();

		/*See if this is actually an instance that we are trying
		  to send to*/
		if (userid.startsWith(";"))
		  continue;
		
		/*Change this to lowercase to facilitate mapping*/
		//Do we really want to do this?
		tempuser = userid.toLowerCase();
		
		if (!SNServer.users.containsKey(tempuser))
		  {
		    missingusers = missingusers.concat(userid+" ");
		  }
	      }

	    if (missingusers.length() == 0)
	      {
		//return ok status if made it to at least one
		//or should just return if found the user?
		SendResponseMsg(out, message.MESSAGE, 200, "Message Received");
	      }
	    else
	      {
		/*User was not found.*/
		SendResponseMsg(out, message.MESSAGE, 404, "User(s): "+missingusers+"Not Found");
		break;
	      }
	    
	    /*Add VIA entry*/
	    ViaEntity localvia = new ViaEntity();
	    localvia.setHost(SNServer.localhostname);
	    localvia.setPort(SNServer.localport);
	    try
	      {
		message.addVia(localvia);
	      }
	    catch (MalformedMessageException e)
	      {
		System.err.println("Error adding local via: "+e);
		//Should send error back to sender
		break;
	      }
	    
	    /*See if user is in the hashtable*/
	    for (cnt = 0; cnt < userURIs.length; cnt++)
	      {
		Enumeration recipienthostlist;
		HostRecord recipienthost;
		
		userid = userURIs[cnt].getUser();

		/*See if this is actually an instance that we are trying
		  to send to*/
		if (userid.startsWith(";"))
		  {
		    Enumeration possiblerecipientlist;
		    UserRecord tempurec;
		    
		    /*Walk through all of the users, and if they are
		      subscribing to this, or *, send them the message
		      on all of their hosts.*/
		    possiblerecipientlist = SNServer.users.elements();
		    
		    while (true)
		      {
			try
			  {
			    tempurec = (UserRecord)possiblerecipientlist.nextElement();

			    /*Are they subscribing to this instance or *?*/
			    if (tempurec.isSubscribedTo(userid.substring(1)))
			      {
				/*For now, send this instance message to
				  all locations where user is logged on.*/

				recipienthostlist = tempurec.getLocation();
				
				while (true)
				  {
				    try
				      {
					recipienthost =
					  (HostRecord)recipienthostlist.nextElement();
					
					try
					  {
					    if (DebugDefine.DEBUG)
					      System.out.println("Forwarding message to: "+recipienthost.getHostname()+":"+recipienthost.getPort());
					    Socket ssock =
					      new Socket(recipienthost.getHostname(),
							 recipienthost.getPort());
					    message.writeMessage(ssock.getOutputStream());
					    if (DebugDefine.DEBUG)
					      System.out.println("Waiting for response");
					    rspmsg.readMessage(ssock.getInputStream());
					    if (DebugDefine.DEBUG)
					      System.out.println("Got response");
					  }
					catch (UnknownHostException e)
					  {
					    System.err.println(e);
					    continue;
					  }
					catch (IOException e)
					  {
					    System.err.println(e);
					    continue;
					  }
					catch (MalformedMessageException e)
					  {
					    System.err.println(e);
					    continue;
					  }
					
					if (rspmsg.getMessageType()
					    != rspmsg.RESPONSE)
					  {
					    System.err.println("Failed to receive response message.");
					    continue;
					  }
					
					if (rspmsg.getStatusCode() != 200)
					  {
					    System.err.println("Error Forwarding Message to Host: "+rspmsg.getStatusCode());
					    System.err.println("Response: "+rspmsg.getReasonPhrase());
					    continue;
					  }
				      }
				    catch (NoSuchElementException e)
				      {
					break;
				      }
				  }
			      }
			  }
			catch (NoSuchElementException e)
			  {
			    break;
			  }
		      }
		  }
		else
		  {
		    /*Change this to lowercase to facilitate mapping*/
		    //Do we really want to do this?
		    userid = userid.toLowerCase();
		    
		    /*Retrieve the user record.*/
		    recipient = (UserRecord)SNServer.users.get(userid);
		    recipienthostlist = recipient.getLocation();

		    /*Send the message to each of the hosts that the
		      recipient is registered to.*/
		    while (true)
		      {
			try
			  {
			    recipienthost =
			      (HostRecord)recipienthostlist.nextElement();
			    
			    try
			      {
				SIPurl tempurl = new SIPurl();
				try
				  {
				    tempurl.fromString(userid+"@"+recipienthost.getHostname()+":"+recipienthost.getPort());
				  }
				catch (MalformedSIPurlException e)
				  {
				  }

				message.setRequestURI(new SIPurl[] {tempurl});

				if (DebugDefine.DEBUG)
				  System.out.println("Forwarding message to: "+recipienthost.getHostname()+":"+recipienthost.getPort());
				Socket ssock =
				  new Socket(recipienthost.getHostname(),
					     recipienthost.getPort());
				message.writeMessage(ssock.getOutputStream());
				if (DebugDefine.DEBUG)
				  System.out.println("Waiting for response");
				rspmsg.readMessage(ssock.getInputStream());
				if (DebugDefine.DEBUG)
				  System.out.println("Got response");
			      }
			    catch (UnknownHostException e)
			      {
				System.err.println(e);
				continue;
			      }
			    catch (IOException e)
			      {
				System.err.println(e);
				continue;
			      }
			    catch (MalformedMessageException e)
			      {
				System.err.println(e);
				continue;
			      }
			    
			    if (rspmsg.getMessageType() != rspmsg.RESPONSE)
			      {
				System.err.println("Failed to receive response message.");
				continue;
			      }
			    
			    if (rspmsg.getStatusCode() != 200)
			      {
				System.err.println("Error Forwarding Message to Host: "+rspmsg.getStatusCode());
				System.err.println("Response: "+rspmsg.getReasonPhrase());
				continue;
			      }
			  }
			catch (NoSuchElementException e)
			  {
			    break;
			  }
		      }
		  }
	      }

	    break;
	  case message.LOCATE:
	    rspmsg = new SIPMessage();
	    ViaEntity via = new ViaEntity();
	    
	    rspmsg.setMessageType(rspmsg.RESPONSE);
	    rspmsg.setMethod(rspmsg.LOCATE);

	    rspmsg.setStatusCode(200);
	    rspmsg.setReasonPhrase("Locate Succeeded");
	    
	    via.setHost(SNServer.localhostname);
	    via.setPort(SNServer.localport);
	    
	    try
	      {
		rspmsg.addVia(via);
	      }
	    catch (MalformedMessageException e)
	      {
		//Return a system error
		System.err.println(e);
		return;
	      }

	    /*Get the user name (pg 24)*/
	    userURIs = message.getRequestURI();

	    /*Process all users in URI*/
	    for (cnt = 0; cnt < userURIs.length; cnt++)
	      {
		Enumeration recipienthostlist;
		HostRecord recipienthost;
		
		userid = userURIs[cnt].getUser();
		/*Change this to lowercase to facilitate mapping*/
		//Do we really want to do this?
		userid = userid.toLowerCase();


		/*Retrieve the user record.*/
		recipient = (UserRecord)SNServer.users.get(userid);
		if (recipient == null)
		  continue;
		recipienthostlist = recipient.getLocation();

		/*Get location of each of the hosts that the
		  recipient is registered to.*/
		while (true)
		  {
		    try
		      {
			SIPurl userhere = new SIPurl();
			
			recipienthost =
			  (HostRecord)recipienthostlist.nextElement();
			
			userhere.setUser(userid);
			userhere.setHost(recipienthost.getHostname());
			userhere.setPort(recipienthost.getPort());
			rspmsg.addLocation(userhere);
		      }
		    catch (NoSuchElementException e)
		      {
			break;
		      }
		  }
	      }
	    try
	      {
		rspmsg.writeMessage(out);
	      }
	    catch (MalformedMessageException e)
	      {
		System.err.println(e);
		return;
	      }
	    break;
	  default:
	    System.err.println("Unsupported message type, ignoring.");
	  }

	if (DebugDefine.DEBUG)
	  {
	    System.out.println("Terminating connection.");
	    System.out.println();
	  }
	try
	  {
	    client.close();
	  }
	catch (IOException e)
	  {
	    System.err.println("I/O Error --> " + e);
	  }
      }
    catch (MalformedMessageException e)
      {
	System.err.println(e);
	if (DebugDefine.DEBUG)
	  System.out.println("Terminating connection.");
	try
	  {
	    client.close();
	  }
	catch (IOException ie)
	  {
	    System.err.println("I/O Error --> " + ie);
	  }
      }
  }

  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);

    via.setHost(SNServer.localhostname);
    via.setPort(SNServer.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;
      }

  }
}
