package talk;

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

/**
   <P>A TalkTransact object represents a pair of talk daemons; a local
   and a remote daemon.  All communication to the daemons can be done
   through this class.  This class is only used to establish a
   connection with a remote user.</P>

   <P>Use the following protocol to establish a connection from a caller
   to a callee.</P>

   <P> 1) Create a TalkTransact object, lets call it "talkd".  It is
   created with the remote username, hostname, and optionally tty of
   the callee the caller wants to connect with.</P>

   <P> 2) Call talkd.lookUp() to check if the callee already has an
   invitation waiting for the caller.  If they do, lookUp will return
   a Socket already connected to the remote user, otherwise it will
   return null.  If lookUp returned a socket, you're done, you can now
   communicate directly with the callee over the socket.  If not, go
   on to the next steps.</P>

   <P> 3) Create a ServerSocket, ssock, to wait for a connection.  

   <P> 4) First call talkd.announce() to request that the remote talk
   daemon announce to the callee that caller wants to talk to
   them.</P>

   <P> 5) Call talkd.leaveInvite() with the address and port number of
   the ServerSocket created in step 3.</P>

   <P> 6) Wait for thirty seconds on ssock.accept(), and if still
   haven't gotten a connection from the callee, go back to step 4.</P>

   <P> 7) If get a connection on ssock, then call
   talkd.deleteInvite(MINE) to remove the invitation from the talk
   daemon, and proceed with the connection with the callee.</P>

   <P>Even if the connection is canelled before the callee responds to
   an invitation, you should call deleteInvite to delete the
   invitation from the talk daemon.</P>

   <P>Assuming the clients are now connected, there is still a little
   more protocol, which is not handled by TalkTransact.  The first 3
   bytes that the clients send to each other have a special meaning.
   The first byte sent is the character the client uses as backspace.
   The second byte sent is the character used to delete the current
   line, and the third byte is the character used to delete the
   previous word.  So, the first thing each client does on connecting
   is send the 3 control bytes, and then read the 3 bytes from the
   other client.  After this, each client simply sends all characters
   typed by the user to the other client, and prints all characters
   received from the other client, until the connection is closed.</P>

*/

public class TalkTransact {
  /**
     Max length for a username passed to TalkTransact. */
  public static final int MAX_NAME_LEN = CtlMsg.NAME_SIZE;
  /**
     Max length of the tty name passed to TalkTransact. */
  public static final int MAX_TTY_LEN = CtlMsg.TTY_SIZE;

  private java.util.logging.Logger logger = Logger.getLogger();
      
  /**
     Creates a Transaction object which is set up to communicate with
     the remote user specified by the arguments.  This does not actually
     connect to the remote host, though it does do an address lookup on the
     remote host, so it may hang waiting for the lookup to complete.
    
     @param rhost the remote hostname
     @param ruser the remote user
     @param rtty the remote tty, can also be null or empty string
     @param lhost the local hostname
     @param luser the local user's username
   
     @exception IOException on I/O error.
   */
  public TalkTransact(String rhost, String ruser, String rtty,
		      String lhost, String luser)
    throws IOException {
    hisHostname = rhost;
    hisName = ruser;
    hisTTY = rtty;
    hisAddress = InetAddress.getByName(hisHostname);

    myName = luser;
    myAddress = getMyAddress(hisAddress);
    logger.finer("Using local address " + myAddress);
    myHostname = lhost;

    ctlSocket = new DatagramSocket(0, myAddress);

    // set up the CtlMsg, except for addr, which is passed in by
    // leaveInvite
    ctlMsg = new CtlMsg();
    ctlMsg.ctlAddr = new SockaddrIn(ctlSocket.getLocalPort(),
				    ctlSocket.getLocalAddress());
    ctlMsg.addr = new SockaddrIn();
    ctlMsg.pid = hashCode();		 // just needs to be some code
    ctlMsg.localName = myName;
    ctlMsg.remoteName = hisName;
    ctlMsg.remoteTTY = hisTTY;
    logger.finest("ctlMsg.ctlAddr = "+ctlMsg.ctlAddr);
  }

  /**
     Returns the network address of the local interface that will be
     used to connect to <TT>remoteAddress</TT>.
  */
  private InetAddress getMyAddress(InetAddress remoteAddress)
    throws IOException
  {
    DatagramSocket sock = new DatagramSocket();
    sock.connect(remoteAddress, talkdPort);
    InetAddress addr = sock.getLocalAddress();
    sock.disconnect();
    return addr;
  }

  public InetAddress getLocalAddress() {
    return myAddress;
  }
  
  /**
     Performs a lookup on the remote talk daemon to see if there's already
     an invitation waiting.  If so, returns a Socket object connected to the
     remote client, which can be used to communicate with the callee.  If
     there was no invite waiting, then returns null.  Call getLastResponseStr
     to get a string describing why the lookup failed.

     @return a Socket object which can be used to communicate with the
     callee if the lookup was successful, else null.
     
     @exception IOException if socket fails to connect
  */
  public synchronized Socket lookUp() throws IOException {
    CtlResponse response = transact(CtlMsg.LOOK_UP, hisAddress);
    if (response.answer == CtlResponse.SUCCESS) {
      remoteId = response.idNum;
      return new Socket(response.addr.addr, response.addr.port);
    } else {
      return null;
    }
  }

  /**
     Leaves an invitation with the local talk daemon for a callee.  If the
     callee does a lookUp, they will receive this address/port pair in
     response, and can try to connect to it.

     @exception IOException if communication with talk daemon fails.
   */
  public synchronized void leaveInvite(InetAddress addr, int port)
    throws IOException {
    ctlMsg.addr.addr = addr;
    ctlMsg.addr.port = port;
    CtlResponse response = transact(CtlMsg.LEAVE_INVITE, myAddress);
    localId = response.idNum;
  }

  public static final int MINE = 0;
  public static final int HIS = 1;

  /**
     Delete an invitation from a talk daemon.  If <code>which</code> is
     MINE, deletes it from the local talk daemon, if it's HIS, deletes it
     from the remote daemon.

     @exception IOException on I/O error.
  */
  public synchronized void deleteInvite(int whose) throws IOException {
    InetAddress addr;
    if (whose == MINE) {
      addr = myAddress;
      ctlMsg.idNum = localId;
    } else {
      addr = hisAddress;
      ctlMsg.idNum = remoteId;
    }
    transact(CtlMsg.DELETE, addr);
  }

  /**
     Ask the remote talk daemon to announce to the callee that the caller
     wants to talk to him.

     @return true if talk daemon agreed, false if the talk daemon refused.
     Call getLastResponseStr to get string describing why the talk daemon
     refused.

     @exception IOException on I/O errror.
  */
  public synchronized boolean announce() throws IOException {
    ctlMsg.idNum = lastAnnounce + 1;
    CtlResponse response = transact(CtlMsg.ANNOUNCE, hisAddress);
    if (response.answer == CtlResponse.SUCCESS) {
      lastAnnounce = response.idNum;
      return true;
    } else
      return false;
  }

  /**
     Returns the response code from the last talk transaction.

     @return the code returned by the last talk transaction.  The return
     value is one of SUCCESS, NOT_HERE, FAILED, MACHINE_UNKNOWN,
     PERMISSION_DENIED, UNKOWN_REQUEST, BADVERION, BADADDR, or BADCTLADDR.
  */
  public synchronized int getLastResponse() {
    return lastResponse;
  }

  /**
     Returns a string describing the status of the last transaction. */
  public synchronized String getLastResponseStr() {
    return lastResponseStr;
  }
  
  private CtlResponse transact(int request, InetAddress addr)
    throws IOException {
    String[] args = {Integer.toString(request), addr.toString()};
    logger.entering("TalkTransact", "transact", args);

    ctlMsg.type = (byte)request;
    CtlResponse response = new CtlResponse();
    response.version = -1;

    // create a datagram packet
    DatagramPacket op =
      new DatagramPacket(ctlMsg.writeData(), CtlMsg.length, addr, talkdPort);
    DatagramPacket ip =
      new DatagramPacket(new byte[CtlResponse.length], CtlResponse.length);
    

    do {
      ctlSocket.setSoTimeout(4000);		 // 4 seconds
      // resend until a response is obtained
      while (true) {
	ctlSocket.send(op);
	try {
	  ctlSocket.receive(ip);
	  break;
	} catch (InterruptedIOException e) {}
      }
      
      response.readData(ip.getData());
      ctlSocket.setSoTimeout(1000); // 1 second
      
      // read any more duplicate packets that are queued up now.
      while ((ip.getLength() < CtlResponse.length) ||
	     !(response.version == TalkInfo.TALK_VERSION &&
	       response.type == request)) {
	try {
	  ctlSocket.receive(ip);
	} catch (InterruptedIOException e) {
	  // if it got interrupted then there's no more to read
	  break;
	}
      }
    } while (!(response.version == TalkInfo.TALK_VERSION &&
	       response.type == request));

    lastResponse = response.answer;
    lastResponseStr = responses[response.answer];
    return response;
  }

  /**
     Returns a string representation of this, consisting of TalkTransact@,
     followed by the hashcode for this, followed by the local and remote
     addresses.  */
  public synchronized String toString() {
    return "TalkTransact@" + Integer.toHexString(hashCode()) + ": " +
      myName + "@" + myHostname + " ==> " +
      hisName + "@" + hisHostname + " " + hisTTY;
  }

  /**
     This is just for testing the class out.  Call it with the same
     arguments that would be passed to the constructor, and it will put it
     through its paces.
  */
  public static void main(String[] args) {
    if (args.length != 4) {
      System.err.println("usage: talk rhost ruser rtty luser");
      System.exit(1);
    }

    try {
      String
	rhost = args[0],
	ruser = args[1],
	rtty = args[2],
	luser = args[3],
	myHostName = InetAddress.getLocalHost().getHostName();

      System.out.println("Creating a talkd object with parameters "
			 + "rhost="+rhost+" ruser="+ruser
			 + " rtty="+rtty+" lhost="+myHostName
			 + " luser="+luser);
      TalkTransact talkd =
	new TalkTransact(args[0], args[1], args[2], myHostName, args[3]);
      System.out.println("talkd created: "+talkd);
      Socket sock = talkd.lookUp();
      
      if (sock == null) {
	System.out.println("Lookup failed");
	ServerSocket ssock = new ServerSocket(0,1,InetAddress.getLocalHost());
	System.out.println("Waiting on port "+ssock.getLocalPort()+
			   ", address "+ssock.getInetAddress());

	do {
	  System.out.println("Announcing...");
	  if (!talkd.announce()) {
	    System.out.println("Announce failed: "+talkd.getLastResponseStr());
	    break;
	  }
	  System.out.println("Going to leave an invitation");
	  talkd.leaveInvite(ssock.getInetAddress(), ssock.getLocalPort());
	  System.out.println("Waiting 30 seconds for a connection...");
	  ssock.setSoTimeout(30000);
	  try {
	    sock = ssock.accept();
	    System.out.println("Accepted a connection");
	  } catch (InterruptedIOException e) {}
	} while (sock == null);
	talkd.deleteInvite(MINE);
	talkd.deleteInvite(HIS);
      } else {
	System.out.println("Lookup succeeded");
      }
    } catch (Exception e) {
      System.out.println("Caught an exception");
      e.printStackTrace();
    }
    System.out.println("Exiting now...");
  }
    
  private String hisHostname = "remotehost";
  private String hisName = "remotename";
  private String hisTTY = "";
  private InetAddress hisAddress;

  private String myHostname = "localhost";
  private String myName = "localname";
  private InetAddress myAddress;
  
  private static final int talkdPort = 518; // ntalk port
  private DatagramSocket ctlSocket; // the socket to remote server

  /* The id #s of local and remote invitations.  This is kept track of
    so can delete them later. */
  private int localId = -1;
  private int remoteId = -1;

  private int lastAnnounce = -2;
  private static final String[] responses = {
    "Success",				                  /* SUCCESS */
    "Your party is not logged on",                        /* NOT_HERE */
    "Target machine is too confused to talk to us",       /* FAILED */
    "Target machine does not recognize us",               /* MACHINE_UNKNOWN */
    "Your party is refusing messages",                    /* PERMISSION_DENIED */
    "Target machine can not handle remote talk",          /* UNKNOWN_REQUEST */
    "Target machine indicates protocol mismatch",         /* BADVERSION */
    "Target machine indicates protocol botch (addr)",     /* BADADDR */
    "Target machine indicates protocol botch (ctl_addr)", /* BADCTLADDR */
  };
  private String lastResponseStr = responses[CtlResponse.SUCCESS];
  private int lastResponse;

  
  CtlMsg ctlMsg;
}

