/** 
 * 
 * Copyright 2004 Protique Ltd
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); 
 * you may not use this file except in compliance with the License. 
 * You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 * 
 **/

package org.activemq.transport.tcp;

import EDU.oswego.cs.dl.util.concurrent.BoundedBuffer;
import EDU.oswego.cs.dl.util.concurrent.BoundedChannel;
import EDU.oswego.cs.dl.util.concurrent.BoundedLinkedQueue;
import EDU.oswego.cs.dl.util.concurrent.Executor;
import EDU.oswego.cs.dl.util.concurrent.PooledExecutor;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.io.WireFormat;
import org.activemq.io.WireFormatLoader;
import org.activemq.message.Packet;
import org.activemq.transport.TransportChannelSupport;
import org.activemq.transport.TransportStatusEvent;
import org.activemq.util.JMSExceptionHelper;

import javax.jms.JMSException;
import java.io.BufferedInputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.EOFException;
import java.io.IOException;
import java.io.InterruptedIOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.UnknownHostException;

/**
 * A tcp implementation of a TransportChannel
 *
 * @version $Revision: 1.2 $
 */
public class TcpTransportChannel extends TransportChannelSupport implements Runnable {
    private static final int DEFAULT_SOCKET_BUFFER_SIZE = 64 * 1024;
    private static final Log log = LogFactory.getLog(TcpTransportChannel.class);
    protected Socket socket;
    protected DataOutputStream dataOut;
    protected DataInputStream dataIn;

    private WireFormatLoader wireFormatLoader;
    private SynchronizedBoolean closed;
    private SynchronizedBoolean started;
    private Object outboundLock;
    private Executor executor;
    private Thread thread;
    private boolean useAsyncSend = false;
    private int soTimeout = 10000;
    private int socketBufferSize = DEFAULT_SOCKET_BUFFER_SIZE;
    private BoundedChannel exceptionsList;
    private TcpTransportServerChannel serverChannel;

    /**
     * Construct basic helpers
     *
     * @param wireFormat
     */
    protected TcpTransportChannel(WireFormat wireFormat) {
        super(wireFormat);
        this.wireFormatLoader = new WireFormatLoader(wireFormat);
        closed = new SynchronizedBoolean(false);
        started = new SynchronizedBoolean(false);
        // there's not much point logging all exceptions, lets just keep a few around
        exceptionsList = new BoundedLinkedQueue(10);
        outboundLock = new Object();
        setUseAsyncSend(useAsyncSend);
        super.setCachingEnabled(true);
    }

    /**
     * Connect to a remote Node - e.g. a Broker
     *
     * @param wireFormat
     * @param remoteLocation
     * @throws JMSException
     */
    public TcpTransportChannel(WireFormat wireFormat, URI remoteLocation) throws JMSException {
        this(wireFormat);
        try {
            this.socket = createSocket(remoteLocation);
            initializeStreams();
        }
        catch (Exception ioe) {
            throw JMSExceptionHelper.newJMSException("Initialization of TcpTransportChannel failed. " + "URI was: "
                    + remoteLocation + " Reason: " + ioe, ioe);
        }
    }

    /**
     * Connect to a remote Node - e.g. a Broker
     *
     * @param wireFormat
     * @param remoteLocation
     * @param localLocation  - e.g. local InetAddress and local port
     * @throws JMSException
     */
    public TcpTransportChannel(WireFormat wireFormat, URI remoteLocation, URI localLocation) throws JMSException {
        this(wireFormat);
        try {
            this.socket = createSocket(remoteLocation, localLocation);
            initializeStreams();
        }
        catch (Exception ioe) {
            throw JMSExceptionHelper.newJMSException("Initialization of TcpTransportChannel failed: " + ioe, ioe);
        }
    }

   /**
    * Initialize from a ServerSocket
    * @param serverChannel
    * @param wireFormat
    * @param socket
    * @param executor
    * @throws JMSException
    */
    public TcpTransportChannel(TcpTransportServerChannel serverChannel,WireFormat wireFormat, Socket socket, Executor executor) throws JMSException {
        this(wireFormat);
        this.socket = socket;
        this.executor = executor;
        this.serverChannel = serverChannel;
        setServerSide(true);
        try {
            initialiseSocket(socket);
            initializeStreams();
        }
        catch (IOException ioe) {
            throw JMSExceptionHelper.newJMSException("Initialization of TcpTransportChannel failed: " + ioe, ioe);
        }
    }

    public TcpTransportChannel(WireFormat wireFormat, Socket socket, Executor executor) throws JMSException {
        this(wireFormat);
        this.socket = socket;
        this.executor = executor;
        try {
            initialiseSocket(socket);
            initializeStreams();
        }
        catch (IOException ioe) {
            throw JMSExceptionHelper.newJMSException("Initialization of TcpTransportChannel failed: " + ioe, ioe);
        }
    }

    /**
     * start listeneing for events
     *
     * @throws JMSException if an error occurs
     */
    public void start() throws JMSException {
        if (started.commit(false, true)) {
            thread = new Thread(this, toString());
            try {
                if (isServerSide()) {
                    thread.setDaemon(true);
                    WireFormat wf = wireFormatLoader.getWireFormat(dataIn);
                    if (wf != null) {
                        setWireFormat(wf);
                    }
                    getWireFormat().registerTransportStreams(dataOut, dataIn);
                    getWireFormat().initiateServerSideProtocol();
                }
                else {
                    getWireFormat().registerTransportStreams(dataOut, dataIn);
                    thread.setPriority(Thread.NORM_PRIORITY + 2);
                }
                //enable caching on the wire format
                currentWireFormat.setCachingEnabled(isCachingEnabled());
                thread.start();
                //send the wire format
                if (!isServerSide()) {
                    getWireFormat().initiateClientSideProtocol();
                }
            }
            catch (EOFException e) {
                doClose(e);
            }
            catch (IOException e) {
                JMSException jmsEx = new JMSException("start failed: " + e.getMessage());
                jmsEx.initCause(e);
                jmsEx.setLinkedException(e);
                throw jmsEx;
            }
        }
    }

    /**
     * close the channel
     */
    public void stop() {
        if (closed.commit(false, true)) {
            super.stop();
            try {
                if (executor != null) {
                    stopExecutor(executor);
                }
                closeStreams();
                socket.close();
            }
            catch (Exception e) {
                log.warn("Caught while closing: " + e + ". Now Closed", e);
            }
        }
        closed.set(true);
        if (this.serverChannel != null){
            this.serverChannel.removeClient(this);
        }
    }

    public void forceDisconnect() {
        log.debug("Forcing disconnect");
        if (socket != null && socket.isConnected()) {
            try {
                socket.close();
            }
            catch (IOException e) {
                // Ignore
            }
        }
    }

    /**
     * Asynchronously send a Packet
     *
     * @param packet
     * @throws JMSException
     */
    public void asyncSend(final Packet packet) throws JMSException {
        if (executor != null) {
            try {
                executor.execute(new Runnable() {
                    public void run() {
                        try {
                            if (!isClosed()) {
                                doAsyncSend(packet);
                            }
                        }
                        catch (JMSException e) {
                            try {
                                exceptionsList.put(e);
                            }
                            catch (InterruptedException e1) {
                                log.warn("Failed to add element to exception list: " + e1);
                            }
                        }
                    }
                });
            }
            catch (InterruptedException e) {
                log.info("Caught: " + e, e);
            }
            try {
                JMSException e = (JMSException) exceptionsList.poll(0);
                if (e != null) {
                    throw e;
                }
            }
            catch (InterruptedException e1) {
                log.warn("Failed to remove element to exception list: " + e1);
            }
        }
        else {
            doAsyncSend(packet);
        }
    }

    /**
     * @return false
     */
    public boolean isMulticast() {
        return false;
    }

    /**
     * reads packets from a Socket
     */
    public void run() {
        log.trace("TCP consumer thread starting");
        int count = 0;
        while (!isClosed()) {
            if (isServerSide() && ++count > 500) {
                count = 0;
                Thread.yield();
            }
            try {
                Packet packet = getWireFormat().readPacket(dataIn);
                if (packet != null) {
                    doConsumePacket(packet);
                }
            }
            catch (SocketTimeoutException e) {
                //onAsyncException(JMSExceptionHelper.newJMSException(e));
            }
            catch (InterruptedIOException e) {
                // TODO confirm that this really is a bug in the AS/400 JVM
                // Patch for AS/400 JVM
                // lets ignore these exceptions
                // as they typically just indicate the thread was interupted
                // while waiting for input, not that the socket is in error
                //onAsyncException(JMSExceptionHelper.newJMSException(e));
            }
            catch (IOException e) {
                doClose(e);
            }
        }
    }

    public boolean isClosed() {
        return closed.get();
    }

    /**
     * pretty print for object
     *
     * @return String representation of this object
     */
    public String toString() {
        return "TcpTransportChannel: " + socket;
    }

    /**
     * @return the socket used by the TcpTransportChannel
     */
    public Socket getSocket() {
        return socket;
    }

    /**
     * Can this wireformat process packets of this version
     *
     * @param version the version number to test
     * @return true if can accept the version
     */
    public boolean canProcessWireFormatVersion(int version) {
        return getWireFormat().canProcessWireFormatVersion(version);
    }

    /**
     * @return the current version of this wire format
     */
    public int getCurrentWireFormatVersion() {
        return getWireFormat().getCurrentWireFormatVersion();
    }

    // Properties
    //-------------------------------------------------------------------------

    /**
     * @return true if packets are enqueued to a separate queue before dispatching
     */
    public boolean isUseAsyncSend() {
        return useAsyncSend;
    }

    /**
     * set the useAsync flag
     *
     * @param useAsyncSend
     */    
     public void setUseAsyncSend(boolean useAsyncSend) {
        this.useAsyncSend = useAsyncSend;
        try {
            if (useAsyncSend && executor==null ) {
                PooledExecutor pe = new PooledExecutor(new BoundedBuffer(10), 1);
                pe.waitWhenBlocked();
                pe.setKeepAliveTime(1000);
                executor = pe;
            }
            else if (!useAsyncSend && executor != null) {
                stopExecutor(executor);
            }
        }
        catch (Exception e) {
            log.warn("problem closing executor", e);
        }
    }

    

    /**
     * @return the current so timeout used on the socket
     */
    public int getSoTimeout() {
        return soTimeout;
    }

    /**
     * set the socket so timeout
     *
     * @param soTimeout
     * @throws JMSException
     */
    public void setSoTimeout(int soTimeout) throws JMSException {
        this.soTimeout = soTimeout;
        if (this.socket != null){
            try {
                socket.setSoTimeout(soTimeout);
            }
            catch (SocketException e) {
                JMSException jmsEx = new JMSException("Failed to set soTimeout: ", e.getMessage());
                jmsEx.setLinkedException(e);
                throw jmsEx;
            }
        }
    }
    
    /**
     * @param noDelay The noDelay to set.
     */
    public void setNoDelay(boolean noDelay) {
        super.setNoDelay(noDelay);
        if (socket != null){
            try {
                socket.setTcpNoDelay(noDelay);
            }
            catch (SocketException e) {
               log.warn("failed to set noDelay on the socket");//should never happen
            }
        }
    }

    /**
     * @return Returns the socketBufferSize.
     */
    public int getSocketBufferSize() {
        return socketBufferSize;
    }
    /**
     * @param socketBufferSize The socketBufferSize to set.
     */
    public void setSocketBufferSize(int socketBufferSize) {
        this.socketBufferSize = socketBufferSize;
    }
    // Implementation methods
    //-------------------------------------------------------------------------
    /**
     * Actually performs the async send of a packet
     *
     * @param packet
     * @return a response or null
     * @throws JMSException
     */
    protected Packet doAsyncSend(Packet packet) throws JMSException {
        Packet response = null;
        try {
            synchronized (outboundLock) {
                response = getWireFormat().writePacket(packet, dataOut);
                dataOut.flush();
            }
        }
        catch (IOException e) {
//            if (closed.get()) {
//                log.trace("Caught exception while closed: " + e, e);
//            }
//            else {
                JMSException exception = JMSExceptionHelper.newJMSException("asyncSend failed: " + e, e);
                onAsyncException(exception);
                throw exception;
//            }
        }
        catch (JMSException e) {
            if (isClosed()) {
                log.trace("Caught exception while closed: " + e, e);
            }
            else {
                throw e;
            }
        }
        return response;
    }

    protected void doClose(Exception ex) {
        if (!isClosed()) {
            if (!pendingStop) {
                setPendingStop(true);
                setTransportConnected(false);
                if (ex instanceof EOFException) {
                    if (!isServerSide() && !isUsedInternally()){
                        log.warn("Peer closed connection", ex);
                    }
                    fireStatusEvent(new TransportStatusEvent(this,TransportStatusEvent.DISCONNECTED));
                }
                else {
                    fireStatusEvent(new TransportStatusEvent(this,TransportStatusEvent.DISCONNECTED));
                    onAsyncException(JMSExceptionHelper.newJMSException("Error reading socket: " + ex, ex));
                }
            }
            stop();
        }
    }

    /**
     * Configures the socket for use
     * @param sock
     * @throws SocketException
     */
    protected void initialiseSocket(Socket sock) throws SocketException {
        try {
            sock.setReceiveBufferSize(socketBufferSize);
            sock.setSendBufferSize(socketBufferSize);
        }
        catch (SocketException se) {
            log.debug("Cannot set socket buffer size = " + socketBufferSize, se);
        }
        sock.setSoTimeout(soTimeout);
        sock.setTcpNoDelay(isNoDelay());
    }
    
    protected void initializeStreams() throws IOException{
        BufferedInputStream buffIn = new BufferedInputStream(socket.getInputStream(),8192);
        this.dataIn = new DataInputStream(buffIn);
        TcpBufferedOutputStream buffOut = new TcpBufferedOutputStream(socket.getOutputStream(),8192);
        this.dataOut = new DataOutputStream(buffOut);
        fireStatusEvent(new TransportStatusEvent(this,TransportStatusEvent.CONNECTED));
    }

    protected void closeStreams() throws IOException {
        if (dataOut != null) {
            dataOut.close();
        }
        if (dataIn != null) {
            dataIn.close();
        }
    }

    /**
     * Factory method to create a new socket
     *
     * @param remoteLocation the URI to connect to
     * @return the newly created socket
     * @throws UnknownHostException
     * @throws IOException
     */
    protected Socket createSocket(URI remoteLocation) throws UnknownHostException, IOException {
        SocketAddress sockAddress = new InetSocketAddress(remoteLocation.getHost(), remoteLocation.getPort());
        Socket sock = new Socket();
        initialiseSocket(sock);
        sock.connect(sockAddress);
        return sock;
    }

    /**
     * Factory method to create a new socket
     *
     * @param remoteLocation
     * @param localLocation
     * @return @throws IOException
     * @throws IOException
     * @throws UnknownHostException
     */
    protected Socket createSocket(URI remoteLocation, URI localLocation) throws IOException, UnknownHostException {
        SocketAddress sockAddress = new InetSocketAddress(remoteLocation.getHost(), remoteLocation.getPort());
        SocketAddress localAddress = new InetSocketAddress(InetAddress.getByName(localLocation.getHost()), localLocation.getPort());
        Socket sock = new Socket();
        initialiseSocket(sock);
        sock.bind(localAddress);
        sock.connect(sockAddress);
        return sock;
    }

}