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

import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.TimeoutExpiredException;
import org.activemq.io.WireFormat;
import org.activemq.message.Packet;
import org.activemq.message.PacketListener;
import org.activemq.message.Receipt;
import org.activemq.message.ReceiptHolder;
import org.activemq.transport.TransportChannel;
import org.activemq.transport.TransportChannelProvider;
import org.activemq.transport.TransportChannelSupport;
import org.activemq.transport.TransportStatusEvent;
import org.activemq.transport.TransportStatusEventListener;

import javax.jms.ExceptionListener;
import javax.jms.JMSException;
import java.net.URI;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * A Compsite implementation of a TransportChannel
 *
 * @version $Revision: 1.2 $
 */
public class CompositeTransportChannel extends TransportChannelSupport implements TransportStatusEventListener {
    private static final Log log = LogFactory.getLog(CompositeTransportChannel.class);

    protected List uris;
    protected URI currentURI;
    protected TransportChannel channel;
    protected SynchronizedBoolean closed;
    protected SynchronizedBoolean started;
    protected int maximumRetries = 10;
    protected long failureSleepTime = 500L;
    protected long establishConnectionTimeout = 30000L;
    protected long maximumTimeout = 30000L;
    protected boolean incrementTimeout = true;


    public CompositeTransportChannel(WireFormat wireFormat) {
        super(wireFormat);
        this.uris = Collections.synchronizedList(new ArrayList());
        closed = new SynchronizedBoolean(false);
        started = new SynchronizedBoolean(false);
    }

    public CompositeTransportChannel(WireFormat wireFormat, List uris) {
        this(wireFormat);
        this.uris.addAll(uris);
    }

    public String toString() {
        return "CompositeTransportChannel: " + channel;
    }

    public void start() throws JMSException {
        if (started.commit(false, true)) {
//            // Since we could take a LONG time to connect to one of the servers in the connection list,
//            // do the connect async so that the client has a chance to stop() the channel if he needs to shutdown
//            // before a connection is established.
//            new Thread() { 
//                public void run() {
//                    try {
                        establishConnection(establishConnectionTimeout);
//                    } catch (JMSException e) {
//                        if(getExceptionListener()!=null) {
//                            getExceptionListener().onException(e);
//                        }
//                    }
                    fireStatusEvent(new TransportStatusEvent(CompositeTransportChannel.this,TransportStatusEvent.CONNECTED));
//                }
//            }.start();
        }
    }

    /**
     * close the channel
     */
    public void stop() {
        if (closed.commit(false, true)) {
            if (channel != null) {
                try {
                    channel.stop();
                }
                catch (Exception e) {
                    log.warn("Caught while closing: " + e + ". Now Closed", e);
                }
                finally {
                    channel = null;
                    super.stop();
                }
            }
        }
    }
    
    /**
     * Forces disconnect by delegating to the child channel
     */
    public void forceDisconnect() {
    	if (channel != null) channel.forceDisconnect();
    }
    
    public Receipt send(Packet packet) throws JMSException {
        return getChannel().send(packet);
    }


    public Receipt send(Packet packet, int timeout) throws JMSException {
        return getChannel().send(packet, timeout);
    }


    public void asyncSend(Packet packet) throws JMSException {
        getChannel().asyncSend(packet);
    }
    
	public ReceiptHolder asyncSendWithReceipt(Packet packet) throws JMSException {
		return getChannel().asyncSendWithReceipt(packet);
	}

    public void setPacketListener(PacketListener listener) {
        super.setPacketListener(listener);
        if (channel != null) {
            channel.setPacketListener(listener);
        }
    }

    public void setExceptionListener(ExceptionListener listener) {
        super.setExceptionListener(listener);
        if (channel != null) {
            channel.setExceptionListener(listener);
        }
    }


    public boolean isMulticast() {
        return false;
    }

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


    /**
     * Return the maximum amount of time spent trying to establish a connection
     * or a negative number to keep going forever
     *
     * @return
     */
    public long getEstablishConnectionTimeout() {
        return establishConnectionTimeout;
    }

    public void setEstablishConnectionTimeout(long establishConnectionTimeout) {
        this.establishConnectionTimeout = establishConnectionTimeout;
    }

    public int getMaximumRetries() {
        return maximumRetries;
    }

    public void setMaximumRetries(int maximumRetries) {
        this.maximumRetries = maximumRetries;
    }

    public long getFailureSleepTime() {
        return failureSleepTime;
    }

    public void setFailureSleepTime(long failureSleepTime) {
        this.failureSleepTime = failureSleepTime;
    }

    public List getUris() {
        return uris;
    }

    public void setUris(List list) {
        synchronized (uris) {
            uris.clear();
            uris.addAll(list);
        }
    }
    

    /**
     * @return Returns the incrementTimeout.
     */
    public boolean isIncrementTimeout() {
        return incrementTimeout;
    }
    /**
     * @param incrementTimeout The incrementTimeout to set.
     */
    public void setIncrementTimeout(boolean incrementTimeout) {
        this.incrementTimeout = incrementTimeout;
    }
    /**
     * @return Returns the maximumTimeout.
     */
    public long getMaximumTimeout() {
        return maximumTimeout;
    }
    /**
     * @param maximumTimeout The maximumTimeout to set.
     */
    public void setMaximumTimeout(long maximumTimeout) {
        this.maximumTimeout = maximumTimeout;
    }
    
    /**
     * 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 channel != null ? channel.canProcessWireFormatVersion(version) : true;
    }
    
    /**
     * @return the current version of this wire format
     */
    public int getCurrentWireFormatVersion(){
        return channel != null ? channel.getCurrentWireFormatVersion() : 1;
    }

    /**
     * Access to the current channel if one is active
     * @throws JMSException if no channel is available
     */
    public TransportChannel getChannel() throws JMSException {
        if (channel == null) {
            throw new JMSException("No TransportChannel connection available");
        }
        return channel;
    }

    // Implementation methods
    //-------------------------------------------------------------------------
    
    protected void establishConnection(long timeout) throws JMSException {
        // lets try connect
        boolean connected = false;
        long time = failureSleepTime;
        long startTime = System.currentTimeMillis();
        for (int i = 0;!connected && (i < maximumRetries || maximumRetries <= 0) && !closed.get() && !isPendingStop();i++) {
            List list = new ArrayList(getUris());
            if (i > 0) {
                if (maximumRetries > 0 || timeout > 0) {
                    long current = System.currentTimeMillis();
                    if (timeout >= 0) {
                        if (current + time > startTime + timeout) {
                            time = startTime + timeout - current;
                        }
                    }
                    if (current > startTime + timeout || time <= 0) {
                        throw new TimeoutExpiredException("Could not connect to any of the URIs: " + list);
                    }
                }
                log.info("Could not connect; sleeping for: " + time + " millis and trying again");
                try {
                    Thread.sleep(time);
                }
                catch (InterruptedException e) {
                    log.warn("Sleep interupted: " + e, e);
                }
                if (incrementTimeout && time < maximumTimeout) {
                    time *= 2;
                    time = time > maximumTimeout ? maximumTimeout : time;
                }
            }
            while (!connected && !list.isEmpty() && !closed.get() && !isPendingStop()) {
                URI uri = extractURI(list);
                try {
                    attemptToConnect(uri);
                    configureChannel();
                    connected = true;
                    currentURI = uri;
                }
                catch (JMSException e) {
                    log.info("Could not connect to: " + uri + ". Reason: " + e);
                }
            }
        }
        if (!connected && !closed.get()) {
            StringBuffer buffer = new StringBuffer("");
            Object[] uriArray = getUris().toArray();
            for (int i = 0;i < uriArray.length;i++) {
                buffer.append(uriArray[i]);
                if (i < (uriArray.length - 1)) {
                    buffer.append(",");
                }
            }
            JMSException jmsEx = new JMSException("Failed to connect to resource(s): " + buffer.toString());
            throw jmsEx;
        }
    }


    protected void configureChannel() {
        ExceptionListener exceptionListener = getExceptionListener();
        if (exceptionListener != null) {
            channel.setExceptionListener(exceptionListener);
        }
        PacketListener packetListener = getPacketListener();
        if (packetListener != null) {
            channel.setPacketListener(packetListener);
        }
        
        channel.addTransportStatusEventListener(this);
        channel.setCachingEnabled(isCachingEnabled());
        channel.setNoDelay(isNoDelay());
        channel.setUsedInternally(isUsedInternally());
    }

    protected URI extractURI(List list) throws JMSException {
        int idx = 0;
        if (list.size() > 1) {
            do {
                idx = (int) (Math.random() * list.size());
            }
            while (idx < 0 || idx >= list.size());
        }
        return (URI) list.remove(idx);
    }

    protected void attemptToConnect(URI uri) throws JMSException {
        WireFormat wf = currentWireFormat.copy();
        channel = TransportChannelProvider.create(wf, uri);
        if (started.get()) {
            channel.start();
        }
    }
    
	public void statusChanged(TransportStatusEvent event) {
		// Delegate to own listeners
        //set the transport to 'this'
        event.setTransportChannel(this);
		fireStatusEvent(event);
	}
	
	public boolean isTransportConnected() {
		return channel == null ? false : channel.isTransportConnected();
	}
	
	public long getLastReceiptTimestamp() {
		return channel == null ? System.currentTimeMillis() : channel.getLastReceiptTimestamp();
	}
}