/**
 * 
 * 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.codehaus.activemq.transport.tcp;

import EDU.oswego.cs.dl.util.concurrent.BoundedBuffer;
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.codehaus.activemq.io.WireFormat;
import org.codehaus.activemq.transport.TransportServerChannelSupport;
import org.codehaus.activemq.util.JMSExceptionHelper;

import javax.jms.JMSException;
import java.io.IOException;
import java.net.InetAddress;
import java.net.ServerSocket;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.UnknownHostException;

/**
 * Binds to a well known port and listens for Sockets ...
 *
 * @version $Revision: 1.3 $
 */
public class TcpTransportServerChannel extends TransportServerChannelSupport implements Runnable {
    private static final Log log = LogFactory.getLog(TcpTransportServerChannel.class);
    protected static final int DEFAULT_BACKLOG = 500;
    private WireFormat wireFormat;
    private Thread serverSocketThread;
    private ServerSocket serverSocket;
    private SynchronizedBoolean closed;
    private SynchronizedBoolean started;
    private boolean useAsyncSend = false;
    private int maxOutstandingMessages = 10;
    private int backlog = DEFAULT_BACKLOG;

    /**
     * Default Constructor
     *
     * @param bindAddr
     * @throws JMSException
     */
    public TcpTransportServerChannel(WireFormat wireFormat, URI bindAddr) throws JMSException {
        super(bindAddr);
        this.wireFormat = wireFormat;
        closed = new SynchronizedBoolean(false);
        started = new SynchronizedBoolean(false);
        try {
            serverSocket = createServerSocket(bindAddr);
            serverSocket.setSoTimeout(2000);
            updatePhysicalUri(bindAddr);
        }
        catch (Exception se) {
            System.out.println(se);
            se.printStackTrace();
            throw JMSExceptionHelper.newJMSException("Bind to " + bindAddr + " failed: " + se.getMessage(), se);
        }
    }

    public TcpTransportServerChannel(WireFormat wireFormat, ServerSocket serverSocket) throws JMSException {
        super(serverSocket.getInetAddress().toString());
        this.wireFormat = wireFormat;
        this.serverSocket = serverSocket;
        closed = new SynchronizedBoolean(false);
        started = new SynchronizedBoolean(false);
        InetAddress address = serverSocket.getInetAddress();
        try {
            updatePhysicalUri(new URI("tcp", "", address.getHostName(), 0, "", "", ""));
        }
        catch (URISyntaxException e) {
            throw JMSExceptionHelper.newJMSException("Failed to extract URI: : " + e.getMessage(), e);
        }
    }

    public void start() throws JMSException {
        super.start();
        if (started.commit(false, true)) {
            log.info("Listening for connections at: " + getUrl());
            serverSocketThread = new Thread(this, toString());
            serverSocketThread.setDaemon(true);
            serverSocketThread.start();
        }
    }

    public void stop() throws JMSException {
        if (closed.commit(false, true)) {
            super.stop();
            try {
                if (serverSocket != null) {
                    serverSocket.close();
                    serverSocketThread.join();
                    serverSocketThread = null;
                }
            }
            catch (Throwable e) {
                throw JMSExceptionHelper.newJMSException("Failed to stop: " + e, e);
            }
        }
    }

    /**
     * @return pretty print of this
     */
    public String toString() {
        return "TcpTransportServerChannel@" + getUrl();
    }

    /**
     * pull Sockets from the ServerSocket
     */
    public void run() {
        while (!closed.get()) {
            Socket socket = null;
            try {
                socket = serverSocket.accept();
                if (socket != null) {
                    if (closed.get()) {
                        socket.close();
                    }
                    else {
                        // have thread per channel for sending messages and a thread for receiving them
                        PooledExecutor executor = null;
                        if (useAsyncSend) {
                            executor = new PooledExecutor(new BoundedBuffer(maxOutstandingMessages), 1);
                        }
                        TcpTransportChannel channel = new TcpTransportChannel(wireFormat, socket, executor);
                        addClient(channel);
                    }
                }
            }
            catch (SocketTimeoutException ste) {
                //expect this to happen
            }
            catch (Throwable e) {
                if (!closed.get()) {
                    log.warn("run()", e);
                }
            }
        }
    }

    // Properties
    //-------------------------------------------------------------------------
    public boolean isUseAsyncSend() {
        return useAsyncSend;
    }

    public void setUseAsyncSend(boolean useAsyncSend) {
        this.useAsyncSend = useAsyncSend;
    }

    public int getMaxOutstandingMessages() {
        return maxOutstandingMessages;
    }

    public void setMaxOutstandingMessages(int maxOutstandingMessages) {
        this.maxOutstandingMessages = maxOutstandingMessages;
    }

    public int getBacklog() {
        return backlog;
    }

    public void setBacklog(int backlog) {
        this.backlog = backlog;
    }

    public WireFormat getWireFormat() {
        return wireFormat;
    }

    public void setWireFormat(WireFormat wireFormat) {
        this.wireFormat = wireFormat;
    }

    // Implementation methods
    //-------------------------------------------------------------------------
    /**
     * In cases where we construct ourselves with a zero port we need to regenerate the URI with the real physical port
     * so that people can connect to us via discovery
     */
    protected void updatePhysicalUri(URI bindAddr) throws URISyntaxException {
        URI newURI = new URI(bindAddr.getScheme(), bindAddr.getUserInfo(), bindAddr.getHost(), serverSocket
                .getLocalPort(), bindAddr.getPath(), bindAddr.getQuery(), bindAddr.getFragment());
        setUrl(newURI.toString());
    }

    /**
     * Factory method to create a new ServerSocket
     *
     * @throws UnknownHostException
     * @throws IOException
     */
    protected ServerSocket createServerSocket(URI bind) throws UnknownHostException, IOException {
        ServerSocket answer = null;
        String host = bind.getHost();
        host = (host == null || host.length() == 0) ? "localhost" : host;
        InetAddress addr = InetAddress.getByName(host);
        if (addr.equals(InetAddress.getLocalHost())) {
            answer = new ServerSocket(bind.getPort(), backlog);
        }
        else {
            answer = new ServerSocket(bind.getPort(), backlog, addr);
        }
        return answer;
    }
}