/*
 * JBoss, Home of Professional Open Source
 * Copyright 2005, JBoss Inc., and individual contributors as indicated
 * by the @authors tag. See the copyright.txt in the distribution for a
 * full listing of individual contributors.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software 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 software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.jboss.remoting.transport.async;

import java.io.IOException;
import java.io.InterruptedIOException;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.jboss.logging.Logger;
import org.jboss.remoting.ConnectionFailedException;
import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.transport.async.Correlator.FutureResult;
/**
 * a ChannelPool represents a logical connection to a remote uri.
 * - It handles decomposing synchronous requests into async requests.
 * - It pools AsychChannel connections to be able concurrently do multiple
 * asyc sends. 
 *   
 * @author <a href="mailto:hiram@coredevelopers.net">Hiram Chirino</a>
 */
public class ChannelPool {
	private static final Logger log = Logger.getLogger(ChannelPool.class);
	static private long DEFAULT_TIMEOUT = Long.parseLong(
			System.getProperty("org.jboss.remoting.transport.asynch.", "6000000"));

	private final InvokerLocator uri;
	private final List available = new ArrayList();
	private final Correlator responseManager = new Correlator();
	private int connectCount = 0;
	

	/**
	 * @param uri
	 */
	public ChannelPool(InvokerLocator uri) {
		this.uri = uri;
	}

	/**
	 * Used to validate that a connection can be established.
	 * @throws IOException
	 */
	synchronized public void connect() throws IOException {
		log.debug("connect");
		if (connectCount == 0) {
			getNextAvailable().close();
		}
		connectCount++;
	}

	/**
	 * 
	 * Closes all the pooled channels.
	 *
	 */
	public void disconnect() {
		log.debug("disconnect");
		connectCount--;
		if (connectCount == 0) {
			synchronized (available) {
				Iterator iterator = available.iterator();
				while (iterator.hasNext()) {
					PooledAsynchChannel c =
						(PooledAsynchChannel) iterator.next();
					try {
						c.closeInternal();
					} catch (Exception e) {
					}
					iterator.remove();
				}
			}
		}
	}

	/**
	 * An PooledAsynchChannel wraps an AsynchChannel.
	 * The PooledAsynchChannel will trap the close() call to 
	 * return the channel to the pool or potentialy close it.
	 * 
	 * It also maintains usage data to be able to allow
	 * the pool to expire old AsynchChannels.   
	 *
	 * Communication errors will flag the connection to be
	 * removed from the pool. 
	 */
	private class PooledAsynchChannel
		implements ChannelListner {

		private Channel next;
		boolean doCloseInternal;
		long lastUsed = System.currentTimeMillis();

		PooledAsynchChannel(Channel next) {
			this.next = next;
		}

		public void open(InvokerLocator uri)
			throws IOException, ConnectionFailedException {
			try {
				next.open(uri, this);
			} catch (ConnectionFailedException e) {
				doCloseInternal = true;
				throw e;
			} catch (IOException e) {
				doCloseInternal = true;
				throw e;
			}
		}

		public void open()
			throws IOException{
			try {
				next.open(this);
			} catch (ConnectionFailedException e) {
				doCloseInternal = true;
				throw e;
			} catch (IOException e) {
				doCloseInternal = true;
				throw e;
			}
		}

		public void close() throws IOException {
			if (doCloseInternal) {
				// Don't return. really close it out
				closeInternal();
			} else {
				returnToPool(this);
			}
		}

		public void closeInternal() throws IOException {
			next.close();
		}

		public void send(byte[][] data) throws IOException {
			try {
				lastUsed = System.currentTimeMillis();
				next.send(data);
			} catch (IOException e) {
				doCloseInternal = true;
				throw e;
			}
		}

		/*
		 * Fail safe in case connections are not being closed 
		 * normally. 
		 */
		protected void finalize() throws Throwable {
			try {
				closeInternal();
			} catch (IOException ignore) {
			}
			super.finalize();
		}


		public void receiveEvent(byte[] data) throws InterruptedException {
			lastUsed = System.currentTimeMillis();
			dispatch(data);
		}

		/* 
		 * Connection was closed by the remote end.
		 */
		public void closeEvent() {			
			doCloseInternal = true;
			// If it was still the pool remove it.
			synchronized (available) {
				available.remove(this);
			}
			try {
				close();
			} catch (IOException ignore) {
			}
		}
	}

	/**
	 * Associate a channel to the pool.
	 * When a AsynchChannelServer accepts a new channel it will
	 * associate the existing AsynchChannel with the pool.
	 * 
	 * TODO: Add some logic to age out old idle connections.
	 */
	public void associate(Channel c) throws IOException {
		synchronized (available) {
			PooledAsynchChannel channel = new PooledAsynchChannel(c);
			channel.open();
			available.add(channel);
		}
	}

	private void returnToPool(PooledAsynchChannel c) {
		synchronized (available) {
			available.add(c);
		}
	}

	/**
	 * Return the next available AsynchChannel object for a given invocation session.
	 * It will automatically allocate a new AsynchChannel if none are available.
	 *
	 * @return
	 * @throws IOException
	 */
	public PooledAsynchChannel getNextAvailable() throws IOException {
		synchronized (available) {
			if (available.isEmpty() == false) {
				// Remove last to avoid array copy.
				return (PooledAsynchChannel) available.remove(available.size() - 1);
			}
		}
		// not available, make one on demand
		try {
			log.debug("channel connecting to: " + uri);
			PooledAsynchChannel c =
				new PooledAsynchChannel(
					Registry.createAsynchChannel());
			c.open(uri); 
			
			return c;
		} catch (Exception e) {
			log.debug("Connect Failed: ", e);
			if (log.isDebugEnabled()) {
				log.debug("channel connection to: " + uri + " failed", e);
			}
			throw new ConnectionFailedException("("+uri.getLocatorURI()+"): "+e);
		}
	}

	private static final byte DATAGRAM_TYPE=0;
	private static final byte REQUEST_TYPE=1;
	private static final byte RESPONE_TYPE=2;

	static private byte [] getDatagramHeader() {
		return new byte[] {DATAGRAM_TYPE};
	}
	
	static private byte [] getRequestHeader(int requestId) {
		return new byte[] {REQUEST_TYPE, 
			((byte)((requestId >> 0) & 0xFF)),
			((byte)((requestId >> 8) & 0xFF)),
			((byte)((requestId >> 16) & 0xFF)),
			((byte)((requestId >> 24) & 0xFF)),
			};
	}

	static private byte [] getResponseHeader(int requestId) {
		return new byte[] {RESPONE_TYPE, 
			((byte)((requestId >> 0) & 0xFF)),
			((byte)((requestId >> 8) & 0xFF)),
			((byte)((requestId >> 16) & 0xFF)),
			((byte)((requestId >> 24) & 0xFF)),
			};
	}
	
	static private int readInt(byte data[], int offset) {
		int rc=0;
		rc = 	((data[offset+0]&0xFF) << 0) |
			 	((data[offset+1]&0xFF) << 8) |
				((data[offset+2]&0xFF) << 16) |
				((data[offset+3]&0xFF) << 24);
		return rc;
	}

	/**
	 * Used by a PooledAsynchChannel object to dispatch a message.  
	 * 
	 * @param data
	 */
	private void dispatch(byte[] message) throws InterruptedException {
		if( message ==null || message.length==0 ) {
			log.warn("Protocol Error: message too small.");
			return;
		}
		boolean trace = log.isTraceEnabled();
	
		byte data[];
		int requestId=0;
		AsyncServerInvoker invoker=Registry.getServerInvoker();
		switch( message[0] ) {
			case DATAGRAM_TYPE:
				if( invoker == null ) {
					log.warn("Received a datagram but the AsynchServerInvoker has not been registed.");
					return;
				}
				data = new byte[message.length-1];
				System.arraycopy(message,1,data,0,data.length);
				if( trace )
					log.trace("received datagram request data.");
				invoker.dispatchDatagram(data, this);
				return;
			case REQUEST_TYPE:
				if( invoker == null ) {
					log.warn("Received a request but the AsynchServerInvoker has not been registed.");
					return;
				}
			   requestId = readInt(message, 1);
				data = new byte[message.length-5];
				System.arraycopy(message,5,data,0,data.length);
				if( trace )
					log.trace("received request data for request: "+requestId);
				invoker.dispatchRequest(data, this, requestId);
				return;
			case RESPONE_TYPE:
				requestId = readInt(message, 1);
				data = new byte[message.length-5];
				System.arraycopy(message,5,data,0,data.length);
				if( trace )
					log.trace("received response data for request: "+requestId);
				responseManager.dispatchResponse(requestId, data);
				return;
			default:
				log.warn("Protocol Error: unknown message type: "+message[0]);
				return;
		}

	}

	private void safeClose(PooledAsynchChannel c) {
		if (c == null)
			return;
		try {
			c.close();
		} catch (IOException e) {

		}
	}	

	public void sendDatagram(byte[] data) throws IOException {
		PooledAsynchChannel c = getNextAvailable();
		try {
			c.send(new byte[][]{getDatagramHeader(),data});
		} finally {
			safeClose(c);
		}
	}

	public byte[] sendRequest(byte[] data) throws IOException {
		
		PooledAsynchChannel c = getNextAvailable();
		FutureResult requestId = responseManager.getNextFutureResult();
		try {
			if( log.isTraceEnabled() )
				log.trace("sending request data for request: "+requestId.getID());
			c.send(new byte[][]{getRequestHeader(requestId.getID()),data});
		} finally {
			safeClose(c);
		}
		
		try {
			byte result[] = (byte[])requestId.poll(DEFAULT_TIMEOUT);
			if( log.isTraceEnabled() )
				log.trace("response data was corelated for request: "+requestId.getID());
			if( result == null )
				throw new IOException("Request time out.");
			return result; 
		} catch (InterruptedException e) {
			throw new InterruptedIOException(e.getMessage());
		}
		
	}

	public void sendResponse(byte[] data, int requestId) throws IOException {
		PooledAsynchChannel c = getNextAvailable();
		try {
			if( log.isTraceEnabled() )
				log.trace("sending response data for request: "+requestId);
			c.send(new byte[][]{getResponseHeader(requestId),data});
		} finally {
			safeClose(c);
		}
	}

}
