/**
 *
 * Copyright 2004 Hiram Chirino
 * 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.gnet;

import EDU.oswego.cs.dl.util.concurrent.Latch;
import EDU.oswego.cs.dl.util.concurrent.SynchronizedBoolean;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.geronimo.network.SelectorManager;
import org.apache.geronimo.network.protocol.AbstractProtocol;
import org.apache.geronimo.network.protocol.DownPacket;
import org.apache.geronimo.network.protocol.PlainDownPacket;
import org.apache.geronimo.network.protocol.Protocol;
import org.apache.geronimo.network.protocol.ProtocolException;
import org.apache.geronimo.network.protocol.SocketProtocol;
import org.apache.geronimo.network.protocol.UpPacket;
import org.apache.geronimo.pool.ClockPool;
import org.apache.geronimo.pool.ThreadPool;
import org.codehaus.activemq.io.WireFormat;
import org.codehaus.activemq.message.Packet;
import org.codehaus.activemq.transport.TransportChannelSupport;

import javax.jms.JMSException;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.URI;
import java.net.UnknownHostException;
import java.nio.ByteBuffer;
import java.util.ArrayList;

/**
 * An implementation of a TransportChannel which uses the Geronimo network layer
 * for connectivity.
 *
 * @version $Revision: 1.3 $
 */
public class GTransportChannel extends TransportChannelSupport {
    private static final Log log = LogFactory.getLog(GTransportChannel.class);

    private SynchronizedBoolean closed;
    private SynchronizedBoolean started;
    private Protocol protocol;
    private Latch dispatchLatch;
    private ThreadPool threadPool;
    private WireFormat wireFormat;

    /**
     * Construct basic helpers
     */
    protected GTransportChannel(WireFormat wireFormat, ThreadPool tp) {
        this.wireFormat = wireFormat;
        closed = new SynchronizedBoolean(false);
        started = new SynchronizedBoolean(false);
        dispatchLatch = new Latch();
        threadPool = tp;
    }

    /**
     * @param protocol
     */
    public GTransportChannel(WireFormat wireFormat, Protocol protocol, ThreadPool tp) {
        this(wireFormat, tp);
        init(protocol);
    }

    /**
     * @param remoteLocation
     * @param localLocation
     */
    public GTransportChannel(WireFormat wireFormat, URI remoteLocation, URI localLocation,
                             SelectorManager sm, ThreadPool tp, ClockPool cp)
            throws UnknownHostException, ProtocolException {
        this(wireFormat, tp);

/*
		ControlClientProtocolStack clientStack = new ControlClientProtocolStack();
		clientStack.setClassLoader(Thread.currentThread()
				.getContextClassLoader());
		clientStack.setThreadPool(tp);
		clientStack.setClockPool(cp);
		clientStack.setSelectorManager(sm);
*/
        SocketProtocol sp = new SocketProtocol();
        sp.setTimeout(1000 * 30);
        if (localLocation != null) {
            sp.setInterface(new InetSocketAddress(InetAddress
                    .getByName(localLocation.getHost()), localLocation
                    .getPort()));
        }
        sp.setAddress(new InetSocketAddress(InetAddress
                .getByName(remoteLocation.getHost()), remoteLocation
                .getPort()));
        sp.setSelectorManager(sm);
/*
		clientStack.push(sp);
		ControlClientProtocol ccp = new ControlClientProtocol();
		ccp.setTimeout(1000 * 30);
		clientStack.push(ccp);
		clientStack.setup();
*/
//		init(clientStack);
        init(sp);
        sp.setup();
    }

    /**
     * @param protocol
     */
    private void init(Protocol protocol) {
        this.protocol = protocol;
        // Hookup a new Up protocol so we can get the up stream packets.
        protocol.setUpProtocol(new AbstractProtocol() {
            public void setup() {
            }

            public void drain() {
            }

            public void teardown() {
            }

            public void sendUp(final UpPacket p) {
                try {
                    log.trace("AQUIRING: " + dispatchLatch);
                    dispatchLatch.acquire();
                    log.trace("AQUIRED: " + dispatchLatch);

                    dispatch(p);
                }
                catch (InterruptedException e) {
                    log.warn("Caught exception dispatching packet: " + p + ". Reason: "
                            + e, e);
                    // TODO: notify exception listner and close the connection.
                }
            }

            public void sendDown(DownPacket p) throws ProtocolException {
                getDownProtocol().sendDown(p);
            }

            public void flush() throws ProtocolException {
                getDownProtocol().flush();
            }
        });
    }

    private void dispatch(UpPacket p) {
        try {
            // Dont dispatch messages until the channel is started..
            Packet packet = toPacket(p);
            log.trace("<<<< SENDING UP <<<< " + packet);
            if (packet != null) {
                doConsumePacket(packet);
            }
        }
        catch (IOException e) {
            log.warn("Caught exception dispatching packet: " + p + ". Reason: "
                    + e, e);
            // TODO: notify exception listner and close the connection.
        }
    }

    /**
     * close the channel
     */
    public void stop() {
        super.stop();
        if (closed.commit(false, true)) {
            try {
                protocol.drain();
            }
            catch (Exception e) {
                log.trace(toString() + " now closed");
            }
        }
    }

    /**
     * start listeneing for events
     *
     * @throws JMSException if an error occurs
     */
    public void start() throws JMSException {
        if (started.commit(false, true)) {
            // Allow messages to get dispatched.
            dispatchLatch.release();
        }
    }


    /**
     * Asynchronously send a Packet
     *
     * @param packet
     * @throws JMSException
     */
    public void asyncSend(Packet packet) throws JMSException {
        try {
            if (log.isTraceEnabled()) {
                log.trace(">>>> ASYNC SENDING DOWN >>>> " + packet);
            }

            // lets sync for now to avoid multiple threads writing to the same socket
            synchronized (protocol) {
                protocol.sendDown(toPlainDownPacket(packet));
            }
        }
        catch (IOException e) {
            System.out.println("Caught: " + e);
            e.printStackTrace();
            JMSException jmsEx = new JMSException("asyncSend failed "
                    + e.getMessage());
            jmsEx.setLinkedException(e);
            throw jmsEx;
        }
        catch (ProtocolException e) {
            System.out.println("Caught: " + e);
            e.printStackTrace();
            JMSException jmsEx = new JMSException("asyncSend failed "
                    + e.getMessage());
            jmsEx.setLinkedException(e);
            throw jmsEx;
        }
    }

    public boolean isMulticast() {
        return false;
    }

    protected PlainDownPacket toPlainDownPacket(Packet mqpacket)
            throws IOException, JMSException {

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(baos);
        wireFormat.writePacket(mqpacket, dos);
        dos.close();
        ArrayList list = new ArrayList(1);
        ByteBuffer buffer = ByteBuffer.wrap(baos.toByteArray());
        buffer.limit(buffer.capacity());
        list.add(buffer);
        PlainDownPacket packet = new PlainDownPacket();
        packet.setBuffers(list);
        return packet;
    }

    protected Packet toPacket(UpPacket packet) throws IOException {
        final ByteBuffer buffer = packet.getBuffer();
        InputStream is = new InputStream() {
            public int read() {
                if (!buffer.hasRemaining()) {
                    return -1;
                }
                int rc = 0xFF & buffer.get();
                return rc;
            }

            public synchronized int read(byte[] bytes, int off, int len) {
                len = Math.min(len, buffer.remaining());
                buffer.get(bytes, off, len);
                return len;
            }
        };
        DataInputStream dis = new DataInputStream(is);
        return wireFormat.readPacket(dis);
    }

    /**
     * pretty print for object
     *
     * @return String representation of this object
     */
    public String toString() {
        return "GTransportChannel: " + protocol;
    }
    
    /**
     * 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 wireFormat.canProcessWireFormatVersion(version);
    }
    
    /**
     * @return the current version of this wire format
     */
    public int getCurrentWireFormatVersion(){
        return wireFormat.getCurrentWireFormatVersion();
    }

    public void forceDisconnect() {
	    // TODO: implement me.
		throw new RuntimeException("Not yet Implemented.");
	}
    
}