/*
 * 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.bio;

import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;

import org.jboss.logging.Logger;
import org.jboss.remoting.ConnectionFailedException;
import org.jboss.remoting.InvokerLocator;
import org.jboss.remoting.transport.async.Channel;
import org.jboss.remoting.transport.async.ChannelListner;
import org.jboss.remoting.transport.async.Compression;
import org.jboss.remoting.transport.async.Registry;

/**
 * The Blocking implementation of the AsynchChannel interface.  
 * 
 * This implemenation uses the standard Java 1.3 blocking socket IO.
 *
 * @author <a href="mailto:hiram@coredevelopers.net">Hiram Chirino</a>
 */
public class BlockingChannel implements Runnable, Channel {

	private ChannelListner listner;
	private DataInputStream in;
	private DataOutputStream out;
	private Thread worker;
	private Socket socket;
	private InvokerLocator remoteURI;
	private boolean closing=false;
	private int compression=-1;

	static final private Logger log = Logger.getLogger(BlockingChannel.class);

	/* (non-Javadoc)
	 * @see org.jboss.remoting.transport.async.AsynchChannel#open(java.lang.String, org.jboss.remoting.transport.async.AsynchChannelServer)
	 */
	public void open(
		InvokerLocator remoteURI,
		ChannelListner listner)
		throws IOException, ConnectionFailedException {

		if (log.isTraceEnabled())
			log.trace("Connecting to : " + remoteURI);
			
		this.listner = listner;			
		this.remoteURI = remoteURI;
		InetAddress addr = InetAddress.getByName(remoteURI.getHost());
		int port = remoteURI.getPort();

		boolean enableTcpNoDelay = true;		
		if( remoteURI.getParameters()!=null ) {
			if (remoteURI.getParameters().get("tcp.nodelay") != null)
				enableTcpNoDelay =
					!remoteURI.getParameters().get("tcp.nodelay").equals("false");
			if (remoteURI.getParameters().get("compression") != null)
				compression = Integer.parseInt((String)remoteURI.getParameters().get("compression"));
		}		

		socket = new Socket(addr, port);
		socket.setTcpNoDelay(enableTcpNoDelay);

		out =
			new DataOutputStream(
				new BufferedOutputStream(socket.getOutputStream()));
		out.writeUTF(remoteURI.getLocatorURI());
		if( Registry.getDefaultServer() == null )
			out.writeUTF("async://"+socket.getLocalAddress().getHostAddress()+":0");
		else
			out.writeUTF(Registry.getDefaultServer().getLocalURI().getLocatorURI());
		out.flush();
		in =
			new DataInputStream(
				new BufferedInputStream(socket.getInputStream()));				
				
		worker = new Thread(this, "Channel -> "+remoteURI.getLocatorURI());
		worker.setDaemon(true);
		worker.start();

	}

	public void init(InvokerLocator localURI,Socket socket) throws IOException {
		this.socket = socket;
		out =
			new DataOutputStream(
				new BufferedOutputStream(socket.getOutputStream()));
		out.flush();
		in =
			new DataInputStream(
				new BufferedInputStream(socket.getInputStream()));
				
		// Use to get connection options.
		String destURI = in.readUTF();
		// Use in case we need to establish new connections back to 
		// the source vm.  Could be null.
		String sourceURI = in.readUTF();
		
		this.remoteURI = new InvokerLocator(sourceURI);

		if (log.isTraceEnabled()) {
			log.trace("Connected from : " + remoteURI);
			log.trace("Request URI    : " + destURI);
		}
		
		// What options did the client want to use with this connection?		
		InvokerLocator rruri = new InvokerLocator(destURI);		
		boolean enableTcpNoDelay = true;		
		if( rruri.getParameters()!=null ) {
			if (rruri.getParameters().get("tcp.nodelay") != null)
				enableTcpNoDelay =
					!rruri.getParameters().get("tcp.nodelay").equals("false");
			if (rruri.getParameters().get("compression") != null)
				compression = Integer.parseInt((String)rruri.getParameters().get("compression"));
		}		
		socket.setTcpNoDelay(enableTcpNoDelay);

		if (log.isTraceEnabled()) {
			log.trace("Compression level : " + compression);
			log.trace("tcp no delay : " + enableTcpNoDelay);
		}		
	}
	
	public void open(ChannelListner listner) throws IOException {
		this.listner = listner;			
		worker = new Thread(this, "Channel <- "+remoteURI.getLocatorURI());
		worker.setDaemon(true);
		worker.start();
	}

	static int nextId=0;
	/**
	 * @return
	 */
	synchronized private int getNextID() {
		return nextId++;
	}

	/* (non-Javadoc)
	 * @see org.jboss.remoting.transport.async.AsynchChannel#send(byte[][])
	 */
	synchronized public void send(byte[][] data) throws IOException {
		
		if( closing )
			throw new IOException("connection has been closed.");
			
		int len = 0;
		for (int i = 0; i < data.length; i++)
			len += data[i].length;
		
		if( compression==-1 ) {
			// No compression
			
			out.writeInt(len);
			for (int i = 0; i < data.length; i++)
				out.write(data[i]);
				
		} else {
			// Using compression.			
			
			// Consolidate the data into one array.
			byte uncompressed[] = new byte[len];
			int destpos=0;
			for (int i = 0; i < data.length; i++) {
				System.arraycopy(data[i],0,uncompressed,destpos,data[i].length);
				destpos+=data[i].length;
			}
			
			byte[] compressed = Compression.compress(uncompressed,compression);
			
			out.writeInt(compressed.length);
			out.write(compressed);
		}
		
		out.flush();
	}

	/* (non-Javadoc)
	 * @see java.lang.Runnable#run()
	 */
	public void run() {

		try {
			while (true) {
				log.trace("Waiting for message");
				int size = in.readInt();
				// Did the remote end signal a shutdown??
				if( size == -1 ) 
					break;
				
				log.trace("Reading Message");
				byte data[] = new byte[size]; 
				in.readFully(data);

				if( compression!=-1 ) {
					data = Compression.uncompress(data);
				}
				
				listner.receiveEvent(data);
			}
		} catch (IOException e) {
			// The remote end died on us.
		} catch (Throwable e) {
			log.warn("Unhandled Error: ", e);
		} finally {				
			asyncClose();
		}		
		log.trace("Stopped");
	}

	/**
	 * Starts to terminate the connection.  Lets the remote end
	 * know that we are closing.
	 * 
	 * The server side calls this close.  Could be called in response to
	 * 2 events:
	 * - we initiated the close() (so we finish the close)
	 * - An asynch error initiated the close(). (so we start the close process)
	 * We keep state to know if we started the socket close().  
	 */
	synchronized private void asyncClose() {
		
		// socket is null when we finish close()
		if (socket==null)
			return;

		try {
			socket.shutdownInput();

			// were we allready closing??		
			if( closing ) {	
				// both side should be shutdown now.  finish close.
				in.close();
				out.close();
				socket.close();
				in=null;
				out=null;
				socket = null;											
			} else {
				closing=true;
				listner.closeEvent();
			}
		} catch (IOException e) {
			// If the 'nice' shutdown fails at any point,
			// then do the forced shutdown.
			forcedClose();
		}
		
	}
	
	/**
	 * Starts to terminate the connection.  Lets the remote end
	 * know that we are closing.
	 * 
	 * The client side calls this close.  Could be called in response to
	 * 2 events:
	 * - the remote sever initiated the close(). (so we finish the close)
	 * - we initiated the close() (so we wait for the remote side to finish the close)
	 * We keep state to know if we started the socket close().  
	 *   
	 * @see org.jboss.remoting.transport.async.AsynchChannel#close()
	 */
	synchronized public void close() {
		
		// socket is null when we finish close()
		if (socket==null)
			return;			

		try {
			
			// We are shuting down. 
			// Tell the remote to shutdown.
			out.writeInt(-1);
			socket.shutdownOutput();
			
			// were we allready closing??		
			if( closing ) {
				
				// both side should be shutdown now.  finish close.
				in.close();
				out.close();
				socket.close();
				socket = null;											
					
			} else {
				closing=true;
			}
						
		} catch (IOException e) {
			forcedClose();
		} 
	}

	/**
	 * forcibly terminates the connection without telling the remote end 
	 * that the connection is being closed. 
	 */
	private void forcedClose() {
		try {
			in.close();
		} catch (Throwable e) {
		}
		try {
			out.close();
		} catch (Throwable e) {
		}
		try {
			socket.close();
		} catch (Throwable e) {
		}		
		in=null;
		out=null;
		socket = null;											
	}
	
	/**
	 * @return
	 */
	public InvokerLocator getRemoteURI() {
		return remoteURI;
	}

}
