package taskspaces.system;

import java.io.*;
import java.net.*;
import java.util.*;
import java.lang.reflect.*;
/**
 * This class contains methods for all TaskSpaces components
 * to communicate using object streams.
 * This class is the foundation to all the classes in the TaskSpaces system.
 * @author Rob Markel, Matthew Keoshkerian
 * @version 2.0
 */
public class Communicator implements Serializable
{
   /* --== Declare the Constants that will be used for communication between spaces/nodes etc==-- */

   /** Indicator for Space to accept object. */
   protected static final int IN=0;

   /** Indicator for Space to send object. */
   protected static final int OUT=1;

   /** Indicator for Space to store a message. */
   protected static final int STORE_MESSAGE=2;

   /** Indicator for Space to send a message. */
   protected static final int SEND_MESSAGE=3;

   /** Indicator for Space to reset. */
   protected static final int RESET=4;

   /** Indicator for Space to register a Node. */
   protected static final int REGISTER_NODE=5;

   /** Indicator for Space to delete a registered Node. */
   protected static final int DELETE_NODE=6;

   /** Indicator for Space to return the number of registered Nodes. */
   protected static final int NODE_QUERY=7;

   /** Indicator for Space to accept multiple objects over the current connection. */
   protected static final int IN_MANY=8;

   /** Indicator for Space to send multiple objects over the current connection. */
   protected static final int OUT_MANY=9;

   /** Indicator for Space to return an array of idle registered Node addresses. */
   protected static final int GET_IDLE_NODES=10;

   /** Indicator for Space to return an array of all registered Node addresses. */
   protected static final int GET_ALL_NODES=11;

   /** Protocol indicator for Space to accept an Agent. */
   protected static final int AGENT=12;

   /** Protocol indicator for Space to accept URL to load. */
   protected static final int URLLOADER=13;
   
   /** Protocol indicator for Space to accept URLs to load. */
   protected static final int URLLOADERMANY=14;
   
   /* --== Values used in the logging system ==-- */
   
   /** The PrintWriter that outputs the log file. null if nothing specified. */
   protected PrintWriter logWrite;
    
   /** The file name of the log writer. */
   protected String logWriteName;
   
   /** The prefix to the log entry. */
   protected String logPrefix = "";

   /**
    * Returns a java.util.Properties object loaded from a URL.
    * @param s the URL string.
    * @return the URL properties.
    */ 
   public Properties load(String s) throws Exception
   {
      // Create a new Properties object, load the file, and return the object refrence.
      Properties p=new Properties();
      URL u=new URL(s);
      p.load(new BufferedInputStream(u.openStream()));
      return p;
   }

 
   /**
    * Returns a String array from a CSV property.
    * @param s the property string.
    * @return contains the parsed Strings.
    */ 
   public String[] parseProperty(String s)
   {
      // Create a string tokenizer based on the property sent here.
      StringTokenizer st=new StringTokenizer(s,",");
      
      // Count the amount of tokens and create an array based on the size.
      int tokens=st.countTokens();
      String[] strings=new String[tokens];
      
      // Iterate through to place all tokens into the array of strings and return the array.
      for(int i=0; i<tokens; i++)
      {
        strings[i]=st.nextToken();
      }
      
      return strings;
   }

   /**
    * Returns the list of spaces from a system properties file.
    * @return contains the address:port pairs of registered spaces.
    */ 
   public String[] getSpaces() throws Exception
   {
      Properties p=new Properties();
      // Create a URL based on the System property "propsAddress".
      URL u=new URL(System.getProperty("propsAddress"));
      
      // Load the properties file return the parsed property "spaces"
      p.load(new BufferedInputStream(u.openStream()));
      return parseProperty(p.getProperty("spaces"));
   }
   
   /**
    * Returns the codeserver's URL as listed in the properties file.
    * @return the URL of the codeserver.
    */
   public String getCodeServer() throws Exception
   {
       Properties p = new Properties();
       URL u = new URL(System.getProperty("propsAddress"));
       
       p.load(new BufferedInputStream(u.openStream()));
       return p.getProperty("codeserver");
   }

   /**
    * Sends an object to a given IP address and port.
    * @param ip the destination IP address.
    * @param o the object to be sent.
    * @return void
    */
   public void send(String ip,Object o) throws Exception {sendAnObject(ip,o,IN);}

   /**
    * Sends an Agent to a given IP address and port.
    * @param ip the destination IP address.
    * @param o the Agent object to be sent.
    * @return void
    */
   public void sendAgent(String ip,Object o) throws Exception {sendAnObject(ip,o,AGENT);}
    
   /**
    * Sends the URL to the Space to be loaded.
    * @param ip the destination IP address.
    * @param o the URL of the class to be loaded.
    * @return void
    */
    public void sendURL(String ip,URL o) throws Exception { print("sending URL");sendAnObject(ip,(Object)o,URLLOADER);print("Sent"); }
    
    /**
     * Send the URLs to the Space to be loaded.
     * @param ip the destination IP address.
     * @param o the URLs of the classes to be loaded.
     * @return void
     */
    public void sendURL(String ip,URL[] o) throws Exception { sendAnObject(ip,(Object)o,URLLOADERMANY); }

   /**
    * Sends a key and message to a given Node IP address and port.
    * @param ip the destination IP address.
    * @param key a unique message key.
    * @param message a message.
    * @return void
    */
   public void sendDirectMessage(String ip,Object key,Object message) throws Exception
   {
      // Create a new socket, get it's Ouptut stream, write the key and the message, then flush the stream and close it.
      Socket s=getSocket(ip);
      ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
      os.writeObject(key);
      os.writeObject(message);
      os.flush();
      os.close();
      s.close();
   }
  
   /**
    * Sends a key to a given Space IP address and port.
    * @param ip the destination IP address.
    * @param key a unique message key.
    * @param localPort the local Server port.
    * @return void
    */
   public void sendRemoteMessage(String ip,Object key,int localPort) throws Exception
   {
      // Create a new socket, get it's output stream, send the integer indicating type of message, then send key and the port number, then flush and close stream.
      Socket s=getSocket(ip);
      ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
      os.writeInt(STORE_MESSAGE);
      os.writeObject(key);
      os.writeInt(localPort);
      os.flush();
      os.close();
      s.close();
   }

   /**
    * Requests a message with the given key from a given Space IP address and port.
    * @param ip the IP address.
    * @param key a unique message key.
    * @param localPort the local Server port.
    * @return void
    */
   public void receiveRemoteMessage(String ip,Object key,int localPort) throws Exception
   {
      // Create a new socket, get it's output stream, send the integer indicating type of message, then send key and the port number, then flush and close stream.
      Socket s=getSocket(ip);
      ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
      os.writeInt(SEND_MESSAGE);
      os.writeObject(key);
      os.writeInt(localPort);
      os.flush();
      os.close();
      // Close the socket.
      s.close();
   }

   /**
    * Registers a Node with a Space.
    * @param ip the destination IP address.
    * @param key a unique message key.
    * @param _port the local Server port.
    * @return void
    */
   public void registerNode(String ip,int localPort) throws Exception {sendInt(ip,localPort,REGISTER_NODE);}

   /**
    * Unregisters a Node from a Space.
    * @param ip the Space IP address.
    * @param localPort the local Server port.
    * @return void
    */
   public void deleteNode(String ip,int localPort) throws Exception {sendInt(ip,localPort,DELETE_NODE);}

   /**
    * Requests an object from a Space.
    * @param ip the Space IP address.
    * @return the return Object. 
    */
   public Object receive(String ip) throws Exception {return getValue(ip,OUT);}

   /**
    * Resets a Space or Node.
    * @param ip the destination IP address.
    * @return a Task object. 
    */
   public void reset(String ip) throws Exception {sendCommand(ip,RESET);}

   /**
    * Returns an Object cotaining addresses of Nodes registered with a Space.
    * @param ip the Space IP address.
    * @return the returned Object. 
    */
   public Object workerQuery(String ip) throws Exception {return getValue(ip,NODE_QUERY);} 

   /**
    * Sends a command to a Space.
    * @param ip the Space IP address.
    * @param mode the integer command.
    * @return void
    */
   private void sendCommand(String ip,int mode) throws Exception
   {
      // Create a socket, get it's output stream, send the command value, then flush and close stream, and close socket. 
      Socket s=getSocket(ip);
      ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
      os.writeInt(mode);
      os.flush();
      os.close();
      s.close();
   }

   /**
    * Sends a integer to a Space.
    * @param ip the Space IP address.
    * @param i the integer.
    * @param mode the integer command.
    * @return void
    */
   private void sendInt(String ip,int i,int mode) throws Exception
   {
      // Open a socket, get the stream, write the integer and flush/close stream and close the socket.
      Socket s=getSocket(ip);
      ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
      os.writeInt(mode);
      os.writeInt(i);
      os.flush();
      os.close();
      s.close();
   }

   /**
    * Sends an Object to a Space and an integer command.
   	* @param ip the Space IP address.
   	* @param o the Object.
   	* @param mode the integer command.
   	* @return void 
   	*/
   private void sendAnObject(String ip,Object o,int mode) /*throws Exception*/
   {
      // Open a socket, get the stream, write the integer and flush/close stream and close the socket.
      try 
	  {
      	Socket s=getSocket(ip);
      	ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
      	os.writeInt(mode);
      	os.writeObject(o);
      	os.flush();
      	os.close();
      	s.close();
      }
      catch(Exception e) { e.printStackTrace(); }
   }

   /**
    * Sends a key/value Object pair to a Space with an integer command.
    * @param ip the Space IP address.
    * @param key the key Object.
    * @param value the value Object.
    * @param mode the integer command.
    * @return void 
    */
   private void sendKeyValue(String ip,Object key,Object value,int mode) throws Exception
   {
      // Get the socket and it's output stream. 
      Socket s=getSocket(ip);
      ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
      // Write to the stream the command type, then send the key and value. 
      os.writeInt(mode);
      os.writeObject(key);
      os.writeObject(value);
      // Flush and close the stream, close the socket.
      os.flush();
      os.close();
      s.close();
   } 

   /**
    * Returns an Object from Space.
    * @param ip the Space IP address.
    * @param mode the integer command.
    * @return the returned Object. 
    */
   private Object getValue(String ip,int mode) throws Exception
   {
      // Open a socket connection to the ip and get it's Output stream. 
      Socket s=getSocket(ip);
      ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
      
      // Send the request and flush the output stream.
      os.writeInt(mode);
      os.flush();
      
      // Open the input stream and read the object being sent.
      ObjectInputStream is=new ObjectInputStream(s.getInputStream());
      Object o=is.readObject();
      
      // Close the input/output stream and the socket.
      os.close();
      is.close();
      s.close();

      // Return the sent object.
      return o;
   }

   /**
    * Sends an Object to a Node.
    * @param ip the Node IP address.
    * @param o the Object to send.
    * @return true if successful. 
    */
   public boolean sendToNode(String ip,Object o)
   {
      try 
      {
      	// Open up a socket and it's output stream. Write the object to the stream then flush/close the stream and the socket.
      	Socket s = getSocket(ip);
        ObjectOutputStream os=new ObjectOutputStream(s.getOutputStream());
        os.writeObject(o);
        os.flush();
        os.close();
        s.close();
      }
      catch(Exception e) {return false;}
      
      return true;
   }

   /**
    * Convenience method for output. Expanded to allow for logging.
    * @param s String to print.
    * @return void
    */
   public void print(String s)
   {
      System.out.println(s);
      
      // If the logging has been initialized, output to text as well.
      if (logWrite != null)
      {
      	File f = new File(logWriteName);
      	if (!f.exists())
      	{
      		try{
      			logWrite = new PrintWriter(new FileWriter(f));
      		}
      		catch (Exception e) {e.printStackTrace();}
      	}
      	logWrite.println(logPrefix + ": " + s);
      	logWrite.flush();
      }
   }


   /**
    * Creates new Socket from input address in the form of
    * <code>IPAddress:PortNumber</code>
    * @param s an address String.
    * @return the new Socket connection.
    */
   public static Socket getSocket(String s)
   {
      // Create a new socket object.
      Socket sock=null;
      // Hash the sent ip string (which is IP:PORT) out into two parts.
      int index=s.indexOf(':');
      String ip=s.substring(0,index);
      int port=Integer.parseInt(s.substring(index+1,s.length()));
      // Open the socket and return it.
      try
      {
         sock=new Socket(ip,port);
      } catch(Exception e) {e.printStackTrace();}
      return sock;
   }
    
   /**
    * Adds the URL sent to the list of URLs to look for when loading classes.
    * @param u the URL to be added to the list of resources.
    * @param o tha Object of which the resource list will be modified.
    * @return void
    */
   public void addURL(URL u, Object o) throws IOException
   {
   	// Create a Class array holding the type of class used in the URLClassLoader.addURL(URL u) method.
   	Class [] parameters = new Class [] {URL.class};
   	
   	// Derive the class loader of the object sent and get the class object of URLClassLoader. 
    URLClassLoader sysloader = (URLClassLoader)o.getClass().getClassLoader();//ClassLoader.getSystemClassLoader();
    Class sysclass = URLClassLoader.class;

    try
	{
    	// Do a little tweaking to access a protected method to insert the URL as a resource.
    	Method method = sysclass.getDeclaredMethod("addURL", parameters);
       	method.setAccessible(true);
       	method.invoke(sysloader, new Object[]{u});
       	URL [] urls = sysloader.getURLs();
       	for (int i = 0; i < urls.length; i++)
       		System.out.println(urls[i]);
	}
    catch(Throwable t) 
    {
       	t.printStackTrace();
       	throw new IOException("Error, could not add URL to system classloader");
    }
   }
}
