/** 
 * 
 * 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;
import java.net.URI;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;

import javax.jms.ExceptionListener;
import javax.jms.JMSException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.activemq.UnsupportedWireFormatException;
import org.activemq.broker.BrokerConnector;
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.message.WireFormatInfo;
import org.activemq.util.ExecutorHelper;
import EDU.oswego.cs.dl.util.concurrent.ConcurrentHashMap;
import EDU.oswego.cs.dl.util.concurrent.CopyOnWriteArrayList;
import EDU.oswego.cs.dl.util.concurrent.Executor;

/**
 * Some basic functionality, common across most transport implementations of channels
 * 
 * @version $Revision: 1.1.1.1 $
 */
public abstract class TransportChannelSupport implements TransportChannel {
    private static final Log log = LogFactory.getLog(TransportChannelSupport.class);
    private CopyOnWriteArrayList listeners = new CopyOnWriteArrayList();
    private ConcurrentHashMap requestMap = new ConcurrentHashMap();
    private PacketListener packetListener;
    private ExceptionListener exceptionListener;
    private String clientID;
    private TransportChannelListener transportChannelListener;
    private long lastReceiptTimstamp = 0;
    private boolean serverSide;
    protected boolean pendingStop = false;
    protected boolean transportConnected = true;
    protected WireFormat currentWireFormat;
    protected boolean cachingEnabled = false;
    protected boolean noDelay = false;
    protected boolean usedInternally = false; //denotes if transport is used by an internal Connection
    
    
    
    protected TransportChannelSupport(){
    }
    
    protected TransportChannelSupport(WireFormat wf){
        this.currentWireFormat = wf;
    }

    /**
     * Give the TransportChannel a hint it's about to stop
     * 
     * @param pendingStop
     */
    public void setPendingStop(boolean pendingStop) {
        this.pendingStop = pendingStop;
    }

    /**
     * @return true if the channel is about to stop
     */
    public boolean isPendingStop() {
        return pendingStop;
    }
    
    /**
    * set the wire format to be used by this channel
    * @param wireformat
    */
   public void setWireFormat(WireFormat wireformat){
       currentWireFormat = wireformat;
   }
   
   /**
    * Get the current wireformat used by this channel
    * @return the current wire format - or null if not set
    */
   public WireFormat getWireFormat(){
       return currentWireFormat;
   }

    /**
     * close the channel
     */
    public void stop() {
        transportConnected = false;
        Map map = new HashMap(this.requestMap);
        for (Iterator i = map.values().iterator();i.hasNext();) {
            ReceiptHolder rh = (ReceiptHolder) i.next();
            rh.close();
        }
        map.clear();
        requestMap.clear();
        if (transportChannelListener != null) {
            transportChannelListener.removeClient(this);
        }
        exceptionListener = null;
        packetListener = null;
    }

    /**
     * synchronously send a Packet
     * 
     * @param packet
     * @return a Receipt
     * @throws JMSException
     */
    public Receipt send(Packet packet) throws JMSException {
        return send(packet, 0);
    }

    /**
     * Synchronously send a Packet
     * 
     * @param packet packet to send
     * @param timeout amount of time to wait for a receipt
     * @return the Receipt
     * @throws JMSException
     */
    public Receipt send(Packet packet, int timeout) throws JMSException {
        ReceiptHolder rh = asyncSendWithReceipt(packet);
        Receipt result = rh.getReceipt(timeout);
        return result;
    }
    
    /**
     * Asynchronously send a Packet with receipt.
     * 
     * @param packet the packet to send
     * @return a ReceiptHolder for the packet
     * @throws JMSException
     */
	public ReceiptHolder asyncSendWithReceipt(Packet packet) throws JMSException {
        ReceiptHolder rh = new ReceiptHolder();
        requestMap.put(new Short(packet.getId()), rh);
        Packet response = doAsyncSend(packet);
        if (response != null && response instanceof Receipt){
            rh.setReceipt((Receipt)response);
        }
        return rh;
	}

    // Properties
    //-------------------------------------------------------------------------
    /**
     * @return the transportChannelListener
     */
    public TransportChannelListener getTransportChannelListener() {
        return transportChannelListener;
    }

    /**
     * @param transportChannelListener
     */
    public void setTransportChannelListener(TransportChannelListener transportChannelListener) {
        this.transportChannelListener = transportChannelListener;
    }

    /**
     * Add a listener for changes in a channels status
     * 
     * @param listener
     */
    public void addTransportStatusEventListener(TransportStatusEventListener listener) {
        listeners.add(listener);
    }

    /**
     * Remove a listener for changes in a channels status
     * 
     * @param listener
     */
    public void removeTransportStatusEventListener(TransportStatusEventListener listener) {
        listeners.remove(listener);
    }

    /**
     * @return the clientID
     */
    public String getClientID() {
        return clientID;
    }

    /**
     * @param clientID set the clientID
     */
    public void setClientID(String clientID) {
        this.clientID = clientID;
    }

    /**
     * @return the exception listener
     */
    public ExceptionListener getExceptionListener() {
        return exceptionListener;
    }

    /**
     * @return the packet listener
     */
    public PacketListener getPacketListener() {
        return packetListener;
    }

    /**
     * Set a listener for Packets
     * 
     * @param l
     */
    public void setPacketListener(PacketListener l) {
        this.packetListener = l;
    }

    /**
     * Set an exception listener to listen for asynchronously generated exceptions
     * 
     * @param listener
     */
    public void setExceptionListener(ExceptionListener listener) {
        this.exceptionListener = listener;
    }

    /**
     * @return true if server side
     */
    public boolean isServerSide() {
        return serverSide;
    }

    /**
     * @param serverSide
     */
    public void setServerSide(boolean serverSide) {
        this.serverSide = serverSide;
    }
    
    /**
     * @return true if the transport channel is active,
     * this value will be false through reconnecting
     */
    public boolean isTransportConnected(){
        return transportConnected;
    }
    
    protected void setTransportConnected(boolean value){
        transportConnected = value;
    }
    
    /**
     * Some transports rely on an embedded broker (beer based protocols)
     * @return true if an embedded broker required
     */
    public boolean requiresEmbeddedBroker(){
        return false;
    }
    
    /**
     * Some transports that rely on an embedded broker need to
     * create the connector used by the broker
     * @return the BrokerConnector or null if not applicable
     * @throws JMSException
     */
    public BrokerConnector getEmbeddedBrokerConnector() throws JMSException{
        return null;
    }
    
    
    /**
     * @return true if this transport is multicast based (i.e. broadcasts to multiple nodes)
     */
    public boolean isMulticast(){
        return false;
    }
    
    /**
     * 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 true;
    }
    
	public long getLastReceiptTimestamp() {
		return lastReceiptTimstamp;
	}
    
    /**
     * @return Returns the usedInternally.
     */
    public boolean isUsedInternally() {
        return usedInternally;
    }
    /**
     * @param usedInternally The usedInternally to set.
     */
    public void setUsedInternally(boolean usedInternally) {
        this.usedInternally = usedInternally;
    }
    
    /**
     * Does the transport support wire format version info
     * @return
     */
    public boolean doesSupportWireFormatVersioning(){
        return true;
    }
    
    /**
     * @return the current version of this wire format
     */
    public int getCurrentWireFormatVersion(){
        return -1;
    }
    

       
    /**
     * some transports/wire formats will implement their own fragementation
     * @return true unless a transport/wire format supports it's own fragmentation
     */
    public boolean doesSupportMessageFragmentation(){
        return getWireFormat() != null && getWireFormat().doesSupportMessageFragmentation();
    }
    
    
    /**
     * Some transports/wireformats will not be able to understand compressed messages
     * @return true unless a transport/wire format cannot understand compression
     */
    public boolean doesSupportMessageCompression(){
        return getWireFormat() != null && getWireFormat().doesSupportMessageCompression();
    }
    
    // Implementation methods
    //-------------------------------------------------------------------------
    /**
     * consume a packet from the channel
     * 
     * @param packet
     * @throws UnsupportedWireFormatException
     */
    protected void doConsumePacket(Packet packet) {
        doConsumePacket(packet, packetListener);
    }

    protected void doConsumePacket(Packet packet, PacketListener listener) {
        if (!doHandleReceipt(packet) && !doHandleWireFormat(packet)) {
            if (listener != null) {
                listener.consume(packet);
            }
            else {
                log.warn("No packet listener set to receive packets");
            }
        }
    }

    protected boolean doHandleReceipt(Packet packet) {
        boolean result = false;
        if (packet != null) {
            if (packet.isReceipt()) {
            	lastReceiptTimstamp = System.currentTimeMillis();
                result = true;
                Receipt receipt = (Receipt) packet;
                ReceiptHolder rh = (ReceiptHolder) requestMap.remove(new Short(receipt.getCorrelationId()));
                if (rh != null) {
                    rh.setReceipt(receipt);
                }
                else {
                    log.warn("No Packet found to match Receipt correlationId: " + receipt.getCorrelationId());
                }
            }
        }
        return result;
    }

    protected boolean doHandleWireFormat(Packet packet) {
        boolean handled = false;
        if (packet.getPacketType() == Packet.WIRE_FORMAT_INFO) {
            handled = true;
            WireFormatInfo info = (WireFormatInfo) packet;
            if (!canProcessWireFormatVersion(info.getVersion())) {
                setPendingStop(true);
                String errorStr = "Cannot process wire format of version: " + info.getVersion();
                TransportStatusEvent event = new TransportStatusEvent();
                event.setChannelStatus(TransportStatusEvent.FAILED);
                fireStatusEvent(event);
                onAsyncException(new UnsupportedWireFormatException(errorStr));
                stop();
            }
            else {
                if (log.isDebugEnabled()) {
                    log.debug(this + " using wire format version: " + info.getVersion());
                }
            }
        }
        return handled;
    }

    /**
     * send a Packet to the raw underlying transport This method is here to allow specific implementations to override
     * this method
     * 
     * @param packet
     * @return a response or null
     * @throws JMSException
     */
    protected Packet doAsyncSend(Packet packet) throws JMSException {
        asyncSend(packet);
        return null;
    }

    /**
     * Handles an exception thrown while performing async dispatch of messages
     * 
     * @param e
     */
    protected void onAsyncException(JMSException e) {
        if (exceptionListener != null) {
            transportConnected = false;
            exceptionListener.onException(e);
        }
        else {
            log.warn("Caught exception dispatching message and no ExceptionListener registered: " + e, e);
        }
    }

    /**
     * Fire status event to any status event listeners
     * 
     * @param remoteURI
     * @param status
     */
    protected void fireStatusEvent(URI remoteURI, int status) {
        TransportStatusEvent event = new TransportStatusEvent();
        event.setChannelStatus(status);
        event.setRemoteURI(remoteURI);
        fireStatusEvent(event);
    }

    /**
     * Fire status event to any status event listeners
     * 
     * @param event
     */
    protected void fireStatusEvent(TransportStatusEvent event) {
        if (event != null) {
            for (Iterator i = listeners.iterator();i.hasNext();) {
                TransportStatusEventListener l = (TransportStatusEventListener) i.next();
                l.statusChanged(event);
            }
        }
    }

    /**
     * A helper method to stop the execution of an executor
     * 
     * @param executor the executor or null if one is not created yet
     * @throws InterruptedException
     * @throws JMSException
     */
    protected void stopExecutor(Executor executor) throws InterruptedException, JMSException {
        ExecutorHelper.stopExecutor(executor);
    }
    /**
     * @return Returns the cachingEnabled.
     */
    public boolean isCachingEnabled() {
        return cachingEnabled;
    }
    /**
     * @param cachingEnabled The cachingEnabled to set.
     */
    public void setCachingEnabled(boolean cachingEnabled) {
        this.cachingEnabled = cachingEnabled;
    }
    
    /**
     * Inform Transport to send messages as quickly
     * as possible - for Tcp - this means disabling Nagles,
     * which on OSX may provide better performance for sync
     * sends
     * @return Returns the noDelay.
     */
    public boolean isNoDelay() {
        return noDelay;
    }
    /**
     * @param noDelay The noDelay to set.
     */
    public void setNoDelay(boolean noDelay) {
        this.noDelay = noDelay;
    }
}