package talk;

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

class Request {
  Request() {
    time = System.currentTimeMillis();
  }
  Request(CtlMsg req) {
    time = System.currentTimeMillis();
    request = req;
  }
  
  long time;
  CtlMsg request;
}

/**
 * TalkServer implements a java version of the Unix ntalk server.
 * This can be used on OS's which don't come with a talk server, such
 * and MSWin or Macs.
 *
 * TalkServer needs two collections.
 *
 * 1) It needs to keep a collection of invitations left by
 * LEAVE_INVITE.  This can be indexed in three ways.  When a
 * LEAVE_INVITE message arrives, if there is already an invitation
 * with the same local name, remote name, and pid fields which is less
 * than 60 seconds old, then the new invitation is not added.  When a
 * LOOKUP message, lu, arrives, it matches an entry, e, if
 * e.local_name == lu.remote_name and e.remote_name == lu.local_name.
 * And finally, when a DELETE message arrives, only the id_num fields
 * are compared.
 *
 * 2) Keeps a collection of announce requests, indexed by their
 * local_name, remote_name, and pid.  When an ANNOUNCE request
 * arrives, talkd first checks its list of previous announce requests.
 * If there is no matching request from less than 60 seconds ago, then
 * it makes the announce.  If there is a match less than 60 seconds
 * old, then it only makes the announce if the id_num of the new
 * request is greater than the old request.  In this case, it also
 * assigns a new id_num to this request and returns this new id_num in
 * the response.
 *
 * These both should probably just be Vectors.  */
public class TalkServer {
  private static final int MAX_TIME = 60000; // max lifetime for a message
  private static final int talkdPort = 518; // ntalk port
  
  private Vector myInvitations = new Vector();
  private Vector myAnnounces = new Vector();
  private int myNextIdNum = 0;

  private java.util.logging.Logger logger = Logger.getLogger();
  
  public TalkServer() {
  }
  
  public void mainLoop() throws IOException {
    DatagramSocket sock = new DatagramSocket(talkdPort);
    
    while (true) {
      CtlMsg msg = new CtlMsg();
      CtlResponse response;

      //System.out.println("At your service");
      DatagramPacket msgPacket =
	new DatagramPacket(new byte[CtlMsg.length], CtlMsg.length);
      try { sock.receive(msgPacket); }
      catch (IOException e) {
	logger.info("\nFailed to receive a packet");
	continue;
      }
      msg.readData(msgPacket.getData());
      
      switch (msg.type) {
      case CtlMsg.LOOK_UP:
	logger.finest("Got LOOK_UP message: "+msg);
	response = handleLookup(msg);
	break;

      case CtlMsg.LEAVE_INVITE:
	logger.finest("Got LEAVE_INVITE message: "+msg);
	response = handleInvite(msg);
	break;

      case CtlMsg.ANNOUNCE:
	logger.finest("Got ANNOUNCE message: "+msg);
	response = handleAnnounce(msg);
	break;

      case CtlMsg.DELETE:
	logger.finest("Got DELETE message: "+msg);
	response = handleDelete(msg);
	break;

      default:
	logger.severe("\nGot UNKNOWN message: "+msg);
	response = new CtlResponse();
	response.version = 1;
	response.type = msg.type;
	response.answer = CtlResponse.UNKNOWN_REQUEST;
	response.idNum = 0;
	break;
      }

      logger.finer("\nSending response: "+response);
      // now send the response back
      DatagramPacket responsePacket =
	new DatagramPacket(response.writeData(), CtlResponse.length,
			   msg.ctlAddr.addr, msg.ctlAddr.port);
      try { sock.send(responsePacket); }
      catch(IOException e) {
	logger.warning("Failed to send response");
      }
    } // while (true)
  }

  private CtlResponse handleLookup(CtlMsg msg) throws IOException {
    CtlResponse response = new CtlResponse();
    response.version = 1;
    response.type = msg.type;
    response.answer = CtlResponse.NOT_HERE;
    response.idNum = 0;
    
    long currentTime = System.currentTimeMillis();

    int i = 0;
    while (i < myInvitations.size()) {
      Request r = (Request)myInvitations.elementAt(i);
      if (r.time + MAX_TIME < currentTime) {
	// it's too old, remove it.
	myInvitations.removeElementAt(i);
	continue;
      }
      
      if (msg.localName.equals(r.request.remoteName) &&
	  msg.remoteName.equals(r.request.localName)) {
	//logger.finer("\nFound invitation: "+r.request);
	// found it
	response.idNum = r.request.idNum;
	response.addr = r.request.addr;
	response.answer = CtlResponse.SUCCESS;
	return response;
      }
      i++;
    }

    return response;
  }

  private CtlResponse handleInvite(CtlMsg msg) throws IOException {
    CtlResponse response = new CtlResponse();
    response.version = 1;
    response.type = msg.type;
    response.answer = CtlResponse.FAILED;
    response.idNum = 0;
    long currentTime = System.currentTimeMillis();
	
    int i = 0;
    while (i < myInvitations.size()) {
      Request r = (Request)myInvitations.elementAt(i);
      if (r.time + MAX_TIME < currentTime) {
	// it's been around too long, delete it
	myInvitations.removeElementAt(i);
	continue;
      }

      if (r.request.localName.equals(msg.localName) &&
	  r.request.remoteName.equals(msg.remoteName) &&
	  r.request.pid == msg.pid) {
	r.time = currentTime;
	response.answer = CtlResponse.SUCCESS;
	response.idNum = r.request.idNum;
	break;
      }
      i++;
    }

    if (response.answer != CtlResponse.SUCCESS) {
      msg.idNum = newIdNum();
      myInvitations.addElement(new Request(msg));
      response.answer = CtlResponse.SUCCESS;
      response.idNum = msg.idNum;
    }

    return response;
  }

  private CtlResponse handleAnnounce(CtlMsg msg) throws IOException {
    CtlResponse response = new CtlResponse();

    response.version = 1;
    response.type = msg.type;
    // always succeeds except for error, because I'm assuming
    // 1 user per server.
    response.answer = CtlResponse.SUCCESS;
    response.idNum = 0;
    long currentTime = System.currentTimeMillis();

    int i = 0;
    Request request = null;
    while (i < myAnnounces.size()) {
      Request r = (Request)myAnnounces.elementAt(i);
      if (r.time + MAX_TIME < currentTime) {
	// it's been around too long, delete it
	myAnnounces.removeElementAt(i);
	continue;
      }
	    
      if (r.request.localName.equals(msg.localName) &&
	  r.request.remoteName.equals(msg.remoteName) &&
	  r.request.pid == msg.pid) {
	request = r;
	break;
      }
      i++;
    }
    /* Now request is set to matching element in myAnnounces, or
       is null. */
	
    if (request != null) {
      /* if msg.idNum is not greater than any current idNum then this
	 is a duplicate of a message that we already got. */
      if (msg.idNum > request.request.idNum) {
	request.request.idNum = newIdNum();
	request.time = currentTime;
	announce(msg.remoteName, msg.localName, msg.ctlAddr.addr);
      }
      response.idNum = request.request.idNum;
    } else {
      // there was no matching request, add it to myAnnounces
      msg.idNum = newIdNum();
      response.idNum = msg.idNum;
      myAnnounces.addElement(new Request(msg));
      announce(msg.remoteName, msg.localName, msg.ctlAddr.addr);
    }

    return response;
  }

  private CtlResponse handleDelete(CtlMsg msg) throws IOException {
    CtlResponse response = new CtlResponse();
    response.version = 1;
    response.type = msg.type;
    response.answer = CtlResponse.FAILED;
    response.idNum = 0;

    for (int i = 0; i < myInvitations.size(); i++) {
      Request r = (Request)myInvitations.elementAt(i);
      if (r.request.idNum == msg.idNum) {
	myInvitations.removeElementAt(i);
	response.answer = CtlResponse.SUCCESS;
	break;
      }
    }

    return response;
  }

  private int newIdNum() {
    return myNextIdNum++;
  }

  private void announce(String toName, String fromName, InetAddress fromAddr) {
    System.out.println("Talk request for "+toName+" from "
		   +fromName+"@"+fromAddr.getHostName());
  }

  public static void main(String[] args) {
    TalkServer server = new TalkServer();
    try {
      server.mainLoop();
    } catch (IOException e) {
      System.err.println("Caught an exception");
      e.printStackTrace();
    }
  }
}
