package JavaAgent.agent;

import JavaAgent.resource.*;

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

/**
 * Subclass of CommInterface which uses sockets for message passing.
 * When instantiated, a ServerThread is started which creates a ServerSocket 
 * and waits in an infinite while loop waiting for client accepts. Each client
 * accept spawns a MessageThread which reads and interprets the message. Each
 * message transmission starts a ClientThread which retrieves the correct
 * SocketAddress and binds to the remote ServerSocket.<p>
 *
 * <hr>
 * Copyright (c) 1995, H. Robert Frost, Stanford University.
 * All rights reserved.<p>
 * Copyright (c) 1996, H. Robert Frost, Enterprise Integration Technologies,
 * Inc. All rights reserved.<p>
 *
 * RESTRICTED RIGHTS LEGEND: Use, duplication or disclosure by the 
 * Government is subject to restrictions as set forth in 
 * subparagraph(c)(1)(ii) of the Rights in Technical Data and Computer
 * Software clause at DFARS 252.227-7013 and in similar clauses in the
 * FAR and NASA FAR supplement.<p>
 * 
 * This software is bound by the terms and conditions listed in the
 * attached <a href="../LICENSE">LICENSE file</A>.
 * <hr>
 * @author <A HREF="http://cdr.stanford.edu/html/people/frost-bio.html"> Rob Frost</A>
 * @version 0.3 5/21/96 for Java(tm) Language Version 1.0.2
 */

public class SocketInterface extends CommInterface {
 
  /**
   * The port which this SocketInterface is connected to on the local machine.
   */
  public int port = 0;

  /**
   * Server socket.
   */
  protected ServerSocket server;

  /**
   * Server thread.
   */
  protected ServerThread server_thread;

  /**
   * Contructs a SocketInterface for the specified Agent on the specified local
   * port. 
   * @param A Agent which this CommInterface represents.
   * @param port Local port to which the Server Socket should be connected.
   * If zero, connect to an anonymous port.
   */

  public SocketInterface(Agent A, int port) {
     super(A);
     this.port = CreateServer(port);
   }   

  /**
   * Called by the Agent to disconnect the SocketInterface.
   * First stops the ServerThread, then closes the ServerSocket.
   */

  protected void disconnect(){
    try{
      if(server_thread != null){
	server_thread.cleanUp();
      }
      parent.addSystemMessage("SocketInterface disconnected.");
    } catch (Exception e) {
      parent.addSystemMessage("Problem disconnecting server socket.", e);
    }
  }
  
  /**
   * Called to send a given KQMLmessage. A TimerThread is 
   * started which will stop the ClientThread after a specified timeout 
   * interval.
   * @param message The KQMLmessage to send.
   */

  protected void sendMessage (KQMLmessage message){
    try{
      ClientThread ct = new ClientThread("client",parent,message);
      ct.start();
    } catch (Exception e){
      parent.addSystemMessage("Failed to send message.", e);
    }
  }

  /**
   * Creates a Server Socket on the specified port.
   * @param port The local port to which the Server Socket should be bound.
   * @param The actual port to which the Server Socket was bound, or -1 if 
   * there was an error.
   */

  protected int CreateServer(int port){
    int answer = port;
    try{
      server = new ServerSocket(port);
      answer = server.getLocalPort();
      server_thread = new ServerThread("server", parent, this, server);
      server_thread.start();
      parent.addSystemMessage("Connected to network on local port " + answer);
    } catch (Exception e){
      answer = -1;
      parent.addSystemMessage("Failed to create server socket.", e);
    }
    return answer;
  }
}

/**
 * Subclass of Thread which sends out messages. This thread attempts to get
 * a valid address for the message recepient, if no address exists locally 
 * then it sends a message to the ANS for the address and waits for the reply.
 * If the reply is a null address, then the message is scraped, otherwise it 
 * is sent.
 */

class ClientThread extends Thread{
  
  protected Agent parent;
  protected KQMLmessage message;

  public ClientThread(String name, Agent A, KQMLmessage m){
    super(name);
    parent = A;
    message = m;
  }

  public void run(){

    if( !KQMLmessage.testValidity(message)) {
      parent.sendResult(message,false,"Tried to send an incomplete message.");
    } else {
      String receiver = message.getValue("receiver");
      try{
	SocketAddress address;
	if(Interpreter.isSuper(parent.getClass(),"JavaAgent.agent.ANS")){
	  address = (SocketAddress)(parent.getResource("address")).getElement(receiver,RetrievalResource.BLOCK);
	}  else {
	  if(receiver.equals("ANS")){
	    address = (SocketAddress)(parent.getResource("address")).getElement(receiver,RetrievalResource.BLOCK);
	  } else {
	    parent.addSystemMessage("releasePending called in SocketInterface");
	    parent.releasePending();
	    address = (SocketAddress)(parent.getResource("address")).getElement(receiver,RetrievalResource.SEND_MSG);
	  }
	}
	if(address != null){
	  TimerThread tt = new TimerThread(this,AgentParams.SEND_TIMEOUT,parent);
	  tt.start();
	  SendMessage(receiver, message, address.host, address.port);
	  tt.stop();
	  tt = null;
	} else {
	  parent.sendResult(message,false,"No valid address.");
	}   
      } catch (ResourceException e){
	parent.addSystemMessage("Error getting receiver's address.", e);
      }
    }
  }

  /**
   * Sends a KQMLmessage to the specified receiver at the specified host and 
   * port. Reports the success of the transmission to the agent which 
   * made the SendMessage call.
   */
  
  protected void SendMessage(String receiver, KQMLmessage message, 
				     String host, int port){
	
    Socket client = null;
    DataOutputStream output = null;
    
    try{
      client = new Socket(host, port);
    } catch (UnknownHostException e){
      parent.sendResult(message,false, "Address host unknown, " +
			e.toString());
      removeAddress(receiver);
      return;
    } catch (IOException e){
      /* could try resending, for now just abort the message and
	 remove the address */
      parent.sendResult(message,false, "Problem creating client socket, " +
			e.toString());
      removeAddress(receiver);
      return;
    }
      
    try {
      output =
	new DataOutputStream(client.getOutputStream());
    } catch (IOException e){
      parent.sendResult(message,false, "Problem creating output stream, " +
			e.toString());
      cleanUpClient(output,client);
      return;
    }

    try{
      output.writeBytes(message.getSendString());
    } catch (IOException e){
      parent.sendResult(message,false, "Problem writing message, " +
			e.toString());
      cleanUpClient(output,client);
      return;
    }
    
    try{
      output.flush();
    } catch (IOException e){
      parent.sendResult(message,false, "Problem flushing stream, " +
			e.toString());
      cleanUpClient(output,client);
      return;
    }
    
    parent.sendResult(message,true,"Message sent successfully.");
    cleanUpClient(output,client);

  } 

  /**
   * Cleans up the client socket.
   */

  protected void cleanUpClient(DataOutputStream output, Socket client){
    try{
      if(output != null){
	output.close();
      }
    } catch (IOException e){
      parent.addSystemMessage("Error closing output stream.", e);
    }

    try{
      if(client != null){
	client.close();
      }
    } catch (IOException e){
      parent.addSystemMessage("Error closing client socket.",e);
    }
  }

  /**
   * Removes the address for a specified agent, next attempt to send will get
   * a new address. 
   * @param agent Name of Agent whose address is being removed.
   */

  protected void removeAddress(String agent){
    parent.addSystemMessage("Removing address for " + agent);
    parent.getResource("address").removeElement(agent);   
  }

}


/**
 * Subclass of Thread which handles incoming messages. This thread creates a 
 * Server Socket, waits for a client socket to bind and then recursively
 * creates a new ServerThread.
 */

class ServerThread extends Thread{
  
  protected Agent parent;
  protected Socket client = null;
  protected ServerSocket server;
  protected SocketInterface comm;
  protected MessageThread mt = null;
  protected TimerThread tt = null;
  protected DataInputStream input = null;

  public ServerThread(String name, Agent p, SocketInterface si, 
		      ServerSocket ss){
    super(name);
    comm = si;
    parent = p;
    server = ss;
  }
  
  public void run(){ 
  
    while(true){
      /* accept a client connection */
      try{
	if(server != null){
	  client = server.accept();
	} else {
	  break;
	}
      } catch (IOException e){
	parent.addSystemMessage("Socket accept failed.", e);
	continue;
      }
      
      tt = new TimerThread(mt,AgentParams.RECEIVE_TIMEOUT,parent);
      mt = new MessageThread(this,client,parent,tt);
      mt.start();
      tt.start();
    }  
  }

  /**
   * Called to terminate other threads, close streams, etc.
   */

  public void cleanUp(){
    try{
      if(mt != null){
	mt.stop();
	mt = null;
      }
      if(tt != null){
	tt.stop();
	tt = null;
      }
      if(input != null){
	input.close();
      }
      if(client != null){
	client.close();
      }
      stop();
    } catch (IOException e){
      parent.addSystemMessage("Error cleaning up.", e);
    }
  }
}

/**
 * Thread which reads in a message.
 */

class MessageThread extends Thread{

  protected ServerThread parent;
  protected Agent agent;
  protected Socket client;
  protected TimerThread tt;

  /**
   * Reads in a message
   * @param parent Parent thread.
   * @param client Client socket.
   * @param agent Agent whose SocketInterface is being monitored.
   */

  public MessageThread(ServerThread parent, Socket client,
		       Agent agent, TimerThread tt){
    super("MessageThread");
    this.parent = parent;
    this.client = client;
    this.agent = agent;
    this.tt = tt;
  }

  /**
   * Start reading.
   */

  public void run(){
    String message = "";
    DataInputStream input = null;

    /* get the input from the client connection */
    try{
      input = new DataInputStream(client.getInputStream());
    } catch (IOException e){
      agent.addSystemMessage("Problem opening input stream.", e);
      tt.stop();
      return;
    }

    /* Grab input while it is available */
    try{
      String temp = input.readLine();
      if(temp != null)message = message.concat(temp + " ");
      while(input.available() > 0){
	temp = input.readLine();
	if(temp != null)message = message.concat(temp + " ");
      }
      input.close();
      client.close();
    } catch (IOException e){
      agent.addSystemMessage("Error getting input.", e);
      tt.stop();
      return;
    }

    /* If the message has non-empty content, create a KQML message */
    if(!message.equals("")){
      KQMLmessage kmessage =  new KQMLmessage(message); 
      agent.receiveMessage(kmessage);
    } else {
      agent.addSystemMessage("Received empty message.");
    }
    tt.stop();
  }
}


/**
 * Thread which will stop another thread if the timeout interval is 
 * exceeded.
 */

class TimerThread extends Thread{

  protected Thread parent;
  protected long timeout;
  protected Agent agent;

  /**
   * Thread which stops the parent thread after the specified amount of time.
   * @param parent Parent thread.
   * @param timeout The timeout value.
   * @param agent Agent whose SocketInterface is being monitored.
   */

  public TimerThread(Thread parent, long timeout, Agent agent){
    super("Timer Thread");
    this.parent = parent;
    this.timeout = timeout;
    this.agent = agent;
  }

  /**
   * Called to begin timing.
   */

  public void run(){
    try{
      sleep(timeout);
      if(parent != null){ 
	parent.stop();
	parent = null;
      }   
    } catch (InterruptedException e){
      agent.addSystemMessage("Error timing ServerSocket.", e);
    }
  }
}




