/*****************************************************************
 * JADE - Java Agent DEvelopment Framework is a framework to develop
 * multi-agent systems in compliance with the FIPA specifications.
 * Copyright (C) 2000 CSELT S.p.A.
 *
 * The updating of this file to JADE 2.0 has been partially supported by the
 * IST-1999-10211 LEAP Project
 *
 * This file refers to parts of the FIPA 99/00 Agent Message Transport
 * Implementation Copyright (C) 2000, Laboratoire d'Intelligence
 * Artificielle, Ecole Polytechnique Federale de Lausanne
 *
 * GNU Lesser General Public License
 *
 * This library is free software; you can redistribute it sand/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation,
 * version 2.1 of the License.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the
 * Free Software Foundation, Inc., 59 Temple Place - Suite 330,
 * Boston, MA  02111-1307, USA.
 *****************************************************************/

/**
 * KeepAlive.java
 *
 * @author Jose Antonio Exposito
 * @author MARISM-A Development group ( marisma-info@ccd.uab.es )
 * @version 0.1
 * @author Nicolas Lhuillier (Motorola Labs)
 * @version 1.0
 */


package jade.mtp.http;

import jade.mtp.MTPException;
import jade.util.leap.HashMap;
import jade.util.Logger;

import java.io.*;
import java.util.Vector;
import java.net.Socket;
import java.net.InetAddress;
import java.net.UnknownHostException;


/**
 * This class represents a connection to a remote server
 */
public class KeepAlive {
	
	private static Logger logger = Logger.getMyLogger(KeepAlive.class.getName());
	
  /*
   * Inner structure to contain all connection information
   */
	public static class KAConnection {
		private OutputStream   out;
		private InputStream    in;
		private HTTPAddress    address;
		private Vector         connections;
		private int outPort;
		
		KAConnection(HTTPAddress a, int outPort) {
			address = a;
			this.outPort = outPort;
		}
		
		public void open() throws IOException {
			// The address is new or the KA connection was closed
			//HTTPAddress is new or cached missed;
			// Open a new connection
			Socket client;
			//#PJAVA_EXCLUDE_BEGIN
			HTTPSocketFactory sfac = HTTPSocketFactory.getInstance();
			if (outPort > 0) {
				client = sfac.createSocket(address.getHost(),address.getPortNo(),InetAddress.getLocalHost(),outPort);
			} else {
				client = sfac.createSocket(address.getHost(),address.getPortNo());
			}
			
			//#PJAVA_EXCLUDE_END
			/*#PJAVA_INCLUDE_BEGIN
			if (outPort > 0)
			{
				client = new Socket(address.getHost(),address.getPortNo(),InetAddress.getLocalHost(),outPort);
			}
			else {
				client = new Socket(address.getHost(),address.getPortNo());
			}
			#PJAVA_INCLUDE_END*/
			out = new BufferedOutputStream(client.getOutputStream());
			in = new BufferedInputStream(client.getInputStream());
		}
		
		OutputStream getOut() {
			return out;
		}
		
		InputStream getIn() {
			return in;
		}
		
		public HTTPAddress getAddress() {
			return address;
		}
		
		public boolean equals(HTTPAddress a) {
			return address.equals(a);
		}
		
		
		void close() {
			if (isOpen()) {
				try {
					in.close();
					out.close();
				} catch(IOException ioe) {
					if(logger.isLoggable(Logger.WARNING))
						logger.log(Logger.WARNING,"Exception while closing KA connection: "+ioe);
				}
				in = null;
				out = null;
			}
		}
		
		boolean isOpen() {
			return in != null;
		}
		
		void send(byte[] req) throws MTPException {
			try {
				if(logger.isLoggable(Logger.FINER))
					logger.log(Logger.FINER,"Sending HTTP message to: "+ address);
				HTTPIO.writeAll(out,req);
				//Capture the HTTPresponse
				StringBuffer typeConnection = new StringBuffer();
				int code = HTTPIO.getResponseCode(in, typeConnection);
				if (!HTTPIO.KA.equals(typeConnection.toString())) {
					// Close the connection
					if(logger.isLoggable(Logger.FINER))
						logger.log(Logger.FINER,"Closing " + typeConnection +" connection to " + address);
					close();
				}
				if (code != 200) {
					if(logger.isLoggable(Logger.FINER))
						logger.log(Logger.FINER,"Not OK: " + code +", Closing connection to " + address);
					close();
					throw new MTPException("Description: ResponseMessage is not OK");
				}
			} catch (IOException e) {
				close();
				throw new MTPException(e.getMessage(), e);
			}
		}
		
		
		
	} // End of KAConnection inner class
	
	private final Vector   connections;
	private final int      dim;
	private final int      outPort;
	private final boolean  agressive;
	private final HashMap locks = new HashMap();
	
	/** Constructor */
	public KeepAlive(int dim, int outPort, boolean agressive) {
		connections = new Vector(dim);
		this.dim = dim;
		this.outPort = outPort;
		this.agressive = agressive;
	}
	
	/** add a new connection */
	public synchronized void add(KAConnection c) {
		try {
			//The vectors are full.
			if (connections.size() == dim) {
				remove(0); //Remove the first element of vectors, is the older element
			}
			connections.addElement(c);
			//System.out.println("DEBUG: Added Ka conn: "+connections.size()+"/"+dim+" with "+c.getAddress().getPortNo());
		} catch(Exception ioe) {
			if(logger.isLoggable(Logger.WARNING))
				logger.log(Logger.WARNING,ioe.getMessage());
		}
	}
	
	/** delete an exisiting connection, based on position */
	private void remove(int pos) {
		try {
			KAConnection old = getConnection(pos);
			connections.removeElementAt(pos);
			old.close();
		} catch(Exception ioe) {
			if(logger.isLoggable(Logger.WARNING))
				logger.log(Logger.WARNING,ioe.getMessage());
		}
	}
	
	/** delete an exisiting connection, based on its address */
	public synchronized void remove(HTTPAddress addr) {
		connections.removeElement(search(addr));
	}
	
	/** delete an exisiting connection*/
	public synchronized void remove(KAConnection ka) {
		connections.removeElement(ka);
	}
	
	
	
	/** get the socket of the connection when addr make matching */
	private KAConnection getConnection(int pos) {
		return (KAConnection)connections.elementAt(pos);
	}
	
	private KAConnection search(HTTPAddress addr) {
		if (addr != null) {
			KAConnection c;
			for(int i=(connections.size()-1); i >= 0; i--) {
				if ((c=(KAConnection)getConnection(i)).equals(addr)) {
					return c;
				}
			}
		}
		return null;
	}
	
	/** get the socket of the connection when addr make matching */
	public KAConnection getConnection(HTTPAddress addr) {
		return search(addr);
	}
	
	private KAConnection createConnection(final HTTPAddress url) throws MTPException {
		KeepAlive.KAConnection kac = null;
		try {
			kac = new KAConnection(url, outPort);
			kac.open();
			return kac;
		} catch (IOException e) {
			//Remove the inputs of KA object for the current address
			if (kac != null) {
				kac.close();
			}
			throw new MTPException(e.getMessage(), e);
		}
	}
	
	/** get the dimension of Vectors */
	public  int getDim(){
		return dim;
	}
	
	/** get the capacity of Vectors */
	public int capacity() {
		//System.out.println("DIMENSION: "+dim+"  "+"TAMVECT: "+addresses.size());
		return (dim - connections.size());
	}
	
	public synchronized void swap(KAConnection c) {
		try {
			//if only have 1 socket isn't necessary make swap function
			if ((dim > 1)&&(!(connections.indexOf(c)==(connections.size()-1)))) {
				//remove the elements at former position
				connections.removeElement(c);
				//put the elements at the end
				connections.addElement(c);
			}
		} catch(Exception ioe) {
			if(logger.isLoggable(Logger.WARNING))
				logger.log(Logger.WARNING,ioe.getMessage());
		}
	}
	
	
	public void send(HTTPAddress url, byte[] request) throws MTPException {
		Object lock = getLock(url);
		synchronized(lock) {
			KeepAlive.KAConnection kac = null;
			// Try to re-use an existing socket
			if (dim > 0) {
				//Search the address in Keep-Alive object
				kac = getConnection(url);
				if (kac != null) {
					try {
						if(logger.isLoggable(Logger.FINER))
							logger.log(Logger.FINER,"Reusing keepAlive for " + url);
						kac.send(request);
						if (kac.isOpen()) {
							//change the priority of socket & another components of keep-alive object
							//Only the policy == AGGRESSIVE
							//HTTPAddress is cached;
							if (agressive) {
								swap(kac);
							}
						} else {
							if(logger.isLoggable(Logger.FINER))
								logger.log(Logger.FINER,"Removing keepAlive for " + url);
							remove(kac);
						}
					} catch (MTPException e) {
						if(logger.isLoggable(Logger.FINER))
							logger.log(Logger.FINER,"Removing keepAlive for " + url);
						remove(kac);
						// retry with new connection
						kac = null;
					}
				}
			}
			if (kac == null) {
				if(logger.isLoggable(Logger.FINER))
					logger.log(Logger.FINER,"Creating connection to " + url);
				kac = createConnection(url);
				// Send out and check response code
				kac.send(request);
				if (kac.isOpen()) {
					if (dim > 0) {
						// Store the new connection
						if(logger.isLoggable(Logger.FINER))
							logger.log(Logger.FINER,"Adding keepAlive for " + url);
						add(kac);
					} else {
						if(logger.isLoggable(Logger.FINER))
							logger.log(Logger.FINER,"Closing open connection for " + url);
						kac.close();
					}
				}
			}
		}
	}
	
	private Object getLock(final HTTPAddress url) {
		Object lock = locks.get(url.getHost());
		if (lock == null) {
			synchronized(this) {
				lock = locks.get(url.getHost());
				if (lock == null) {
					lock = new Object();
					locks.put(url.getHost(), lock);
				}
			}
		}
		return lock;
	}
	
} //End of class KeepAlive
