/**
 * Copyright (C) 2010-2012, FuseSource Corp.  All rights reserved.
 *
 *     http://fusesource.com
 *
 * 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.fusesource.mqtt.client;

import org.fusesource.hawtbuf.Buffer;
import org.fusesource.hawtbuf.HexSupport;
import org.fusesource.hawtbuf.UTF8Buffer;
import org.fusesource.hawtdispatch.Dispatch;
import org.fusesource.hawtdispatch.DispatchQueue;
import org.fusesource.hawtdispatch.Task;
import org.fusesource.hawtdispatch.transport.DefaultTransportListener;
import org.fusesource.hawtdispatch.transport.HeartBeatMonitor;
import org.fusesource.hawtdispatch.transport.SslTransport;
import org.fusesource.hawtdispatch.transport.TcpTransport;
import org.fusesource.hawtdispatch.transport.Transport;
import org.fusesource.mqtt.codec.CONNACK;
import org.fusesource.mqtt.codec.DISCONNECT;
import org.fusesource.mqtt.codec.MQTTFrame;
import org.fusesource.mqtt.codec.MQTTProtocolCodec;
import org.fusesource.mqtt.codec.MessageSupport.Acked;
import org.fusesource.mqtt.codec.PINGREQ;
import org.fusesource.mqtt.codec.PINGRESP;
import org.fusesource.mqtt.codec.PUBACK;
import org.fusesource.mqtt.codec.PUBCOMP;
import org.fusesource.mqtt.codec.PUBLISH;
import org.fusesource.mqtt.codec.PUBREC;
import org.fusesource.mqtt.codec.PUBREL;
import org.fusesource.mqtt.codec.SUBACK;
import org.fusesource.mqtt.codec.SUBSCRIBE;
import org.fusesource.mqtt.codec.UNSUBACK;
import org.fusesource.mqtt.codec.UNSUBSCRIBE;

import javax.net.ssl.SSLContext;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ProtocolException;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

import static org.fusesource.hawtbuf.Buffer.utf8;
import static org.fusesource.hawtdispatch.Dispatch.createQueue;


/**
 * <p>
 * A callback based non/blocking Connection interface to MQTT.
 * </p>
 *
 * @author <a href="http://hiramchirino.com">Hiram Chirino</a>
 */
public class CallbackConnection {
    
    private static class Request {
        final MQTTFrame frame;
        private final short id;
        final Callback cb;

        Request(int id, MQTTFrame frame, Callback cb) {
            this.id = (short) id;
            this.cb = cb;
            this.frame = frame;
        }
    }

    private static final ExtendedListener DEFAULT_LISTENER = new ExtendedListener(){
        public void onConnected() {
        }
        public void onDisconnected() {
        }
        public void onPublish(UTF8Buffer utf8Buffer, Buffer buffer, Runnable runnable) {
            this.onFailure(createListenerNotSetError());
        }
        public void onPublish(UTF8Buffer topic, Buffer body, Callback<Callback<Void>> ack) {
            this.onFailure(createListenerNotSetError());
        }
        public void onFailure(Throwable value) {
            Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), value);
        }
    };

    private final DispatchQueue queue;
    private final MQTT mqtt;
    private Transport transport;
    private ExtendedListener listener = DEFAULT_LISTENER;
    private Runnable refiller;
    private Map<Short, Request> requests = new ConcurrentHashMap<Short, Request>();
    private LinkedList<Request> overflow = new LinkedList<Request>();
    private final HashMap<Short, Callback<Void>> processed = new HashMap<Short, Callback<Void>>();
    private Throwable failure;
    private boolean disconnected = false;
    private HeartBeatMonitor heartBeatMonitor;
    private long pingedAt;
    private long reconnects = 0;
    private final AtomicInteger suspendCount = new AtomicInteger(0);
    private final AtomicInteger suspendChanges = new AtomicInteger(0);

    private final HashMap<UTF8Buffer, QoS> activeSubs = new HashMap<UTF8Buffer, QoS>();


    public CallbackConnection(MQTT mqtt) {
        this.mqtt = mqtt;
        if(this.mqtt.dispatchQueue == null) {
            this.queue = createQueue("mqtt client");
        } else {
            this.queue = this.mqtt.dispatchQueue;
        }
    }

    public void connect(final Callback<Void> cb) {
        assert cb !=null : "Callback should not be null.";

        if( transport!=null ) {
            cb.onFailure(new IllegalStateException("Already connected"));
            return;
        }
        try {
            createTransport(new LoginHandler(cb, true));
        } catch (Throwable e) {
            // This error happens when the MQTT config is invalid, reattempting
            // wont fix this case.
            cb.onFailure(e);
        }
    }

    void reconnect() {
        try {
            // And reconnect.
            createTransport(new LoginHandler(new Callback<Void>() {
                public void onSuccess(Void value) {

                    mqtt.tracer.debug("Restoring MQTT connection state");
                    // Setup a new overflow so that the replay can be sent out before the original overflow list.
                    LinkedList<Request> originalOverflow = overflow;
                    Map<Short, Request> originalRequests = requests;
                    overflow = new LinkedList<Request>();
                    requests = new ConcurrentHashMap<Short, Request>();

                    // Restore any active subscriptions.
                    if (!activeSubs.isEmpty()) {
                        ArrayList<Topic> topics = new ArrayList<Topic>(activeSubs.size());
                        for (Map.Entry<UTF8Buffer, QoS> entry : activeSubs.entrySet()) {
                            topics.add(new Topic(entry.getKey(), entry.getValue()));
                        }
                        send(new SUBSCRIBE().topics(topics.toArray(new Topic[topics.size()])), null);
                    }

                    // Replay any un-acked requests..
                    for (Map.Entry<Short, Request> entry : originalRequests.entrySet()) {
                        MQTTFrame frame = entry.getValue().frame;
                        frame.dup(true); // set the dup flag as these frames were previously transmitted.
                        send(entry.getValue());
                    }

                    // Replay the original overflow
                    for (Request request : originalOverflow) {
                        // Stuff in the overflow never got sent out.. so no need to set the dup flag
                        send(request);
                    }

                }

                public void onFailure(Throwable value) {
                    handleFatalFailure(value);
                }
            }, false));
        } catch (Throwable e) {
            handleFatalFailure(e);
        }
    }
    void handleSessionFailure(Throwable error) {
        // Socket failure, should we try to reconnect?
        if( !disconnected && (mqtt.reconnectAttemptsMax<0 || reconnects < mqtt.reconnectAttemptsMax ) ) {

            mqtt.tracer.debug("Reconnecting transport");
            // Cleanup the previous transport.
            if(heartBeatMonitor!=null) {
                heartBeatMonitor.stop();
                heartBeatMonitor = null;
            }
            final Transport t = transport;
            transport = null;

            if(t!=null) {
                t.stop(new Task() {
                    @Override
                    public void run() {
                        listener.onDisconnected();
                        reconnect();
                    }
                });
            } else {
                reconnect();
            }

        } else {
            // nope.
            handleFatalFailure(error);
        }
    }

    void reconnect(final Callback<Transport> onConnect) {
        long reconnectDelay = mqtt.reconnectDelay;
        if( reconnectDelay> 0 && mqtt.reconnectBackOffMultiplier > 1.0 ) {
            reconnectDelay = (long) Math.pow(mqtt.reconnectDelay*reconnects, mqtt.reconnectBackOffMultiplier);
        }
        reconnectDelay = Math.min(reconnectDelay, mqtt.reconnectDelayMax);
        reconnects += 1;
        queue.executeAfter(reconnectDelay, TimeUnit.MILLISECONDS, new Task() {
            @Override
            public void run() {
                if(disconnected) {
                    onConnect.onFailure(createDisconnectedError());
                } else {
                    try {
                        createTransport(onConnect);
                    } catch (Exception e) {
                        onConnect.onFailure(e);
                    }
                }
            }
        });
    }

    /**
     * Creates and start a transport to the MQTT server.  Passes it to the onConnect
     * once the transport is connected.
     *
     * @param onConnect
     * @throws Exception
     */
    void createTransport(final Callback<Transport> onConnect) throws Exception {
        mqtt.tracer.debug("Connecting");
        String scheme = mqtt.host.getScheme();

        final Transport transport;
        if( "tcp".equals(scheme) ) {
            transport = new TcpTransport();
        }  else if( SslTransport.protocol(scheme)!=null ) {
            SslTransport ssl = new SslTransport();
            if( mqtt.sslContext == null ) {
                mqtt.sslContext = SSLContext.getDefault();
            }
            ssl.setSSLContext(mqtt.sslContext);
            transport = ssl;
        } else {
            throw new Exception("Unsupported URI scheme '"+scheme+"'");
        }

        if( mqtt.blockingExecutor == null ) {
            mqtt.blockingExecutor = MQTT.getBlockingThreadPool();
        }
        transport.setBlockingExecutor(mqtt.blockingExecutor);
        transport.setDispatchQueue(queue);
        transport.setProtocolCodec(new MQTTProtocolCodec());

        if( transport instanceof TcpTransport ) {
            TcpTransport tcp = (TcpTransport)transport;
            tcp.setMaxReadRate(mqtt.maxReadRate);
            tcp.setMaxWriteRate(mqtt.maxWriteRate);
            tcp.setReceiveBufferSize(mqtt.receiveBufferSize);
            tcp.setSendBufferSize(mqtt.sendBufferSize);
            tcp.setTrafficClass(mqtt.trafficClass);
            tcp.setUseLocalHost(mqtt.useLocalHost);
            tcp.connecting(mqtt.host, mqtt.localAddress);
        }

        transport.setTransportListener(new DefaultTransportListener(){
            @Override
            public void onTransportConnected() {
                mqtt.tracer.debug("Transport connected");
                if(disconnected) {
                    onFailure(createDisconnectedError());
                } else {
                    onConnect.onSuccess(transport);
                }
            }

            @Override
            public void onTransportFailure(final IOException error) {
                mqtt.tracer.debug("Transport failure: %s", error);
                onFailure(error);
            }

            private void onFailure(final Throwable error) {
                if(!transport.isClosed()) {
                    transport.stop(new Task() {
                        @Override
                        public void run() {
                            onConnect.onFailure(error);
                        }
                    });
                }
            }
        });
        transport.start(NOOP);
    }

    class LoginHandler implements Callback<Transport> {
        final Callback<Void> cb;
        private final boolean initialConnect;

        LoginHandler(Callback<Void> cb, boolean initialConnect) {
            this.cb = cb;
            this.initialConnect = initialConnect;
        }

        public void onSuccess(final Transport transport) {
            transport.setTransportListener(new DefaultTransportListener() {
                @Override
                public void onTransportFailure(IOException error) {
                    mqtt.tracer.debug("Transport failure: %s", error);
                    transport.stop(NOOP);
                    onFailure(error);
                }

                @Override
                public void onTransportCommand(Object command) {
                    MQTTFrame response = (MQTTFrame) command;
                    mqtt.tracer.onReceive(response);
                    try {
                        switch (response.messageType()) {
                            case CONNACK.TYPE:
                                CONNACK connack = new CONNACK().decode(response);
                                switch (connack.code()) {
                                    case CONNECTION_ACCEPTED:
                                        mqtt.tracer.debug("MQTT login accepted");
                                        onSessionEstablished(transport);
                                        cb.onSuccess(null);
                                        listener.onConnected();
                                        queue.execute(new Task() {
                                            @Override
                                            public void run() {
                                                drainOverflow();
                                            }
                                        });
                                        break;
                                    default:
                                        mqtt.tracer.debug("MQTT login rejected");
                                        // Bad creds or something. No point in reconnecting.
                                        transport.stop(NOOP);
                                        cb.onFailure(new MQTTException("Could not connect: " + connack.code(), connack));
                                }
                                break;
                            default:
                                mqtt.tracer.debug("Received unexpected MQTT frame: %d", response.messageType());
                                // Naughty MQTT server? No point in reconnecting.
                                transport.stop(NOOP);
                                cb.onFailure(new IOException("Could not connect. Received unexpected command: " + response.messageType()));

                        }
                    } catch (ProtocolException e) {
                        mqtt.tracer.debug("Protocol error: %s", e);
                        transport.stop(NOOP);
                        cb.onFailure(e);
                    }
                }
            });
            transport.resumeRead();
            if( mqtt.connect.clientId() == null ) {
                String id = hex(transport.getLocalAddress())+Long.toHexString(System.currentTimeMillis()/1000);
                if(id.length() > 23) {
                    id = id.substring(0,23);
                }
                mqtt.connect.clientId(utf8(id));
            }
            MQTTFrame encoded = mqtt.connect.encode();
            boolean accepted = transport.offer(encoded);
            mqtt.tracer.onSend(encoded);
            mqtt.tracer.debug("Logging in");
            assert accepted: "First frame should always be accepted by the transport";
        }
        
        private boolean tryReconnect() {
            if(initialConnect) {
                return mqtt.connectAttemptsMax<0 || reconnects < mqtt.connectAttemptsMax;
            }
            
            return mqtt.reconnectAttemptsMax<0 || reconnects < mqtt.reconnectAttemptsMax;
        }

        public void onFailure(Throwable value) {
            // Socket failure, should we try to reconnect?
            if( !disconnected && tryReconnect() ) {
                reconnect(this);
            } else {
                // nope.
                cb.onFailure(value);
            }
        }
    }

    boolean onRefillCalled =false;
    public void onSessionEstablished(Transport transport) {
        this.transport = transport;
        if( suspendCount.get() > 0 ) {
            this.transport.suspendRead();
        }
        this.transport.setTransportListener(new DefaultTransportListener() {
            @Override
            public void onTransportCommand(Object command) {
                MQTTFrame frame = (MQTTFrame) command;
                mqtt.tracer.onReceive(frame);
                processFrame(frame);
            }
            @Override
            public void onRefill() {
                onRefillCalled =true;
                drainOverflow();
            }

            @Override
            public void onTransportFailure(IOException error) {
                handleSessionFailure(error);
            }
        });
        pingedAt = 0;
        if(mqtt.getKeepAlive()>0) {
            heartBeatMonitor = new HeartBeatMonitor();
            heartBeatMonitor.setWriteInterval((mqtt.getKeepAlive() * 1000) / 2);
            heartBeatMonitor.setTransport(this.transport);
            heartBeatMonitor.suspendRead(); // to match the suspended state of the transport.
            heartBeatMonitor.setOnKeepAlive(new Task() {
                @Override
                public void run() {
                    // Don't care if the offer is rejected, just means we have data outbound.
                    if(!disconnected && pingedAt==0) {
                        MQTTFrame encoded = new PINGREQ().encode();
                        if(CallbackConnection.this.transport.offer(encoded)) {
                            mqtt.tracer.onSend(encoded);
                            final long now = System.currentTimeMillis();
                            final long suspends = suspendChanges.get();
                            pingedAt = now;
                            queue.executeAfter(CallbackConnection.this.mqtt.getKeepAlive(), TimeUnit.SECONDS, new Task() {
                                @Override
                                public void run() {
                                    if (now == pingedAt) {
                                        // if the connection remained suspend we will never get the ping response..
                                        // Looks like the user has forgoton to resume the connection
                                        if (suspends == suspendChanges.get() && suspendCount.get() > 0) {
                                            handleFatalFailure(new IllegalStateException("The connection has remained suspended for an extended period of time so it cannot do proper keep alive processing.  Did you forget to resume the connection?"));
                                        } else {
                                            mqtt.tracer.debug("Ping timeout");
                                            handleSessionFailure(new ProtocolException("Ping timeout").fillInStackTrace());
                                        }
                                    }
                                }
                            });
                        }
                    }
                    
                }
            });
            heartBeatMonitor.start();
        }
    }

    public Transport transport() {
        return transport;
    }

    public DispatchQueue getDispatchQueue() {
        return queue;
    }

    public void resume() {
        suspendChanges.incrementAndGet();
        if( suspendCount.decrementAndGet() == 0 && this.transport!=null ) {
            this.transport.resumeRead();
            if(this.heartBeatMonitor!=null){
                this.heartBeatMonitor.resumeRead();
            }
        }
    }

    public void suspend() {
        suspendChanges.incrementAndGet();
        if( suspendCount.incrementAndGet() == 1 && this.transport!=null ) {
            this.transport.suspendRead();
            if(this.heartBeatMonitor!=null){
                this.heartBeatMonitor.suspendRead();
            }
        }
    }

    public CallbackConnection refiller(Runnable refiller) {
        queue.assertExecuting();
        this.refiller = refiller;
        return this;
    }

    public CallbackConnection listener(final Listener original) {
        if( original instanceof ExtendedListener ) {
            this.listener = (ExtendedListener) original;
        } else {
            this.listener = new ExtendedListener() {
                public void onPublish(UTF8Buffer topic, Buffer body, final Callback<Callback<Void>> ack) {
                    original.onPublish(topic, body, new Runnable() {
                        public void run() {
                            ack.onSuccess(null);
                        }
                    });
                }
                public void onPublish(UTF8Buffer topic, Buffer body, Runnable ack) {
                    original.onPublish(topic, body, ack);
                }

                public void onConnected() {
                    original.onConnected();
                }

                public void onDisconnected() {
                    original.onDisconnected();
                }

                public void onFailure(Throwable value) {
                    original.onFailure(value);
                }
            };
        }
        return this;
    }

    public boolean full() {
        queue.assertExecuting();
        return this.transport.full();
    }

    public Throwable failure() {
        queue.assertExecuting();
        return failure;
    }

    public void disconnect(final Callback<Void> onComplete) {
        if( disconnected ) {
            if(onComplete!=null){
                onComplete.onSuccess(null);
            }
            return;
        }

        disconnected = true;
        final short requestId = getNextMessageId();
        final Runnable stop = new Runnable() {
            boolean executed = false;
            public void run() {
                if(!executed) {
                    executed = true;
                    requests.remove(requestId);

                    if(heartBeatMonitor!=null) {
                        heartBeatMonitor.stop();
                        heartBeatMonitor = null;
                    }
                    transport.stop(new Task() {
                        @Override
                        public void run() {
                            listener.onDisconnected();
                            if (onComplete != null) {
                                onComplete.onSuccess(null);
                            }
                        }
                    });
                }
            }
        };
        
        Callback<Void> cb = new Callback<Void>() {
            public void onSuccess(Void v) {
                // To make sure DISCONNECT has been flushed out to the socket
                onRefillCalled = false;
                refiller = new Runnable() {
                    public void run() {
                        if(onRefillCalled) {
                            stop.run();
                        }
                    }
                };
                if(transport != null){
                    transport.flush();
                }
            }
            public void onFailure(Throwable value) {
                stop.run();
            }
        };
        
        // Pop the frame into a request so it we get notified
        // of any failures so we continue to stop the transport.
        if(transport!=null) {
            MQTTFrame frame = new DISCONNECT().encode();
            send(new Request(getNextMessageId(), frame, cb));
        } else {
            cb.onSuccess(null);
        }
    }

    /**
     * Kills the connection without a graceful disconnect.
     * @param onComplete
     */
    public void kill(final Callback<Void> onComplete) {
        if( disconnected ) {
            if(onComplete!=null){
                onComplete.onSuccess(null);
            }
            return;
        }
        disconnected = true;
        if(heartBeatMonitor!=null) {
            heartBeatMonitor.stop();
            heartBeatMonitor = null;
        }
        transport.stop(new Task() {
            @Override
            public void run() {
                listener.onDisconnected();
                if (onComplete != null) {
                    onComplete.onSuccess(null);
                }
            }
        });
    }

    public void publish(String topic, byte[] payload, QoS qos, boolean retain, Callback<Void> cb) {
        publish(utf8(topic), new Buffer(payload), qos, retain, cb);
    }

    public void publish(UTF8Buffer topic, Buffer payload, QoS qos, boolean retain, Callback<Void> cb) {
        queue.assertExecuting();
        if( disconnected ) {
            cb.onFailure(createDisconnectedError());
            return;
        }
        PUBLISH command = new PUBLISH().qos(qos).retain(retain);
        command.topicName(topic).payload(payload);
        send(command, cb);
    }

    public void subscribe(final Topic[] topics, Callback<byte[]> cb) {
        if(topics==null) {
            throw new IllegalArgumentException("topics must not be null");
        }
        queue.assertExecuting();
        if( disconnected ) {
            cb.onFailure(createDisconnectedError());
            return;
        }
        if( listener == DEFAULT_LISTENER ) {
            cb.onFailure(createListenerNotSetError());
        } else {
            send(new SUBSCRIBE().topics(topics), new ProxyCallback<byte[]>(cb){
                @Override
                public void onSuccess(byte[] value) {
                    for (Topic topic : topics) {
                        activeSubs.put(topic.name(), topic.qos());
                    }
                    if(next!=null) {
                        next.onSuccess(value);
                    }
                }
            });
        }
    }

    public void unsubscribe(final UTF8Buffer[] topics, Callback<Void> cb) {
        queue.assertExecuting();
        if( disconnected ) {
            cb.onFailure(createDisconnectedError());
            return;
        }
        send(new UNSUBSCRIBE().topics(topics), new ProxyCallback(cb){
            @Override
            public void onSuccess(Object value) {
                for (UTF8Buffer topic : topics) {
                    activeSubs.remove(topic);
                }
                if(next!=null) {
                    next.onSuccess(value);
                }
            }
        });
    }

    private void send(Acked command, Callback cb) {
        short id = 0;
        if(command.qos() != QoS.AT_MOST_ONCE) {
            id = getNextMessageId();
            command.messageId(id);
        }
        send(new Request(id, command.encode(), cb));
    }

    private void send(Request request) {
        if( failure !=null ) {
            if( request.cb!=null ) {
                request.cb.onFailure(failure);
            }
        } else {
            // Put the request in the map before sending it over the wire. 
            if(request.id!=0) {
                this.requests.put(request.id, request);
            }

            if( overflow.isEmpty() && transport!=null && transport.offer(request.frame) ) {
                mqtt.tracer.onSend(request.frame);
                if(request.id==0) {
                    if( request.cb!=null ) {
                        ((Callback<Void>)request.cb).onSuccess(null);
                    }
                    
                }
            } else {
                // Remove it from the request.
                this.requests.remove(request.id);
                overflow.addLast(request);
            }
        }
    }

    short nextMessageId = 1;
    private short getNextMessageId() {
        short rc = nextMessageId;
        nextMessageId++;
        if(nextMessageId==0) {
            nextMessageId=1;
        }
        return rc;
    }

    private void drainOverflow() {
        queue.assertExecuting();
        if( overflow.isEmpty() || transport==null ){
            return;
        }
        Request request;
        while((request=overflow.peek())!=null) {
            if( this.transport.offer(request.frame) ) {
                mqtt.tracer.onSend(request.frame);
                overflow.removeFirst();
                if(request.id==0) {
                    if( request.cb!=null ) {
                        ((Callback<Void>)request.cb).onSuccess(null);
                    }
                } else {
                    this.requests.put(request.id, request);
                }
            } else {
                break;
            }
        }
        if( overflow.isEmpty() ) {
            if( refiller!=null ) {
                try {
                    refiller.run();
                } catch (Throwable e) {
                    Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
                }
            }
        }
    }


    private void completeRequest(short id, byte originalType, Object arg) {
        Request request = requests.remove(id);
        if( request!=null ) {
            assert originalType==request.frame.messageType();
            if(request.cb!=null) {
                if( arg==null ) {
                    ((Callback<Void>)request.cb).onSuccess(null);
                } else {
                    ((Callback<Object>)request.cb).onSuccess(arg);
                }
            }
        } else {
            handleFatalFailure(new ProtocolException("Command from server contained an invalid message id: " + id));
        }
    }

    private void processFrame(MQTTFrame frame) {
        try {
            switch(frame.messageType()) {
                case PUBLISH.TYPE: {
                    PUBLISH publish = new PUBLISH().decode(frame);
                    toReceiver(publish);
                    break;
                }
                case PUBREL.TYPE:{
                    PUBREL ack = new PUBREL().decode(frame);
                    Callback<Void> onRel = processed.remove(ack.messageId());
                    PUBCOMP response = new PUBCOMP();
                    response.messageId(ack.messageId());
                    send(new Request(0, response.encode(), null));
                    if( onRel!=null ) {
                        onRel.onSuccess(null);
                    }
                    break;
                }
                case PUBACK.TYPE:{
                    PUBACK ack = new PUBACK().decode(frame);
                    completeRequest(ack.messageId(), PUBLISH.TYPE, null);
                    break;
                }
                case PUBREC.TYPE:{
                    PUBREC ack = new PUBREC().decode(frame);
                    PUBREL response = new PUBREL();
                    response.messageId(ack.messageId());
                    send(new Request(0, response.encode(), null));
                    break;
                }
                case PUBCOMP.TYPE:{
                    PUBCOMP ack = new PUBCOMP().decode(frame);
                    completeRequest(ack.messageId(), PUBLISH.TYPE, null);
                    break;
                }
                case SUBACK.TYPE: {
                    SUBACK ack = new SUBACK().decode(frame);
                    completeRequest(ack.messageId(), SUBSCRIBE.TYPE, ack.grantedQos());
                    break;
                }
                case UNSUBACK.TYPE: {
                    UNSUBACK ack = new UNSUBACK().decode(frame);
                    completeRequest(ack.messageId(), UNSUBSCRIBE.TYPE, null);
                    break;
                }
                case PINGRESP.TYPE: {
                    pingedAt = 0;
                    break;
                }
                default:
                    throw new ProtocolException("Unexpected MQTT command type: "+frame.messageType());
            }
        } catch (Throwable e) {
            handleFatalFailure(e);
        }
    }

    static public final Task NOOP = Dispatch.NOOP;

    private void toReceiver(final PUBLISH publish) {
        if( listener !=null ) {
            try {
                Callback<Callback<Void>> cb = null;
                switch( publish.qos() ) {
                    case AT_LEAST_ONCE:
                        cb = new Callback<Callback<Void>>() {
                            public void onSuccess(Callback<Void> value) {
                                PUBACK response = new PUBACK();
                                response.messageId(publish.messageId());
                                send(new Request(0, response.encode(), null));
                                if( value !=null ) {
                                    value.onSuccess(null);
                                }
                            }
                            public void onFailure(Throwable value) {
                            }
                        };
                        break;
                    case EXACTLY_ONCE:
                        cb = new Callback<Callback<Void>>() {
                            public void onSuccess(Callback<Void> value) {
                                PUBREC response = new PUBREC();
                                response.messageId(publish.messageId());
                                processed.put(publish.messageId(), value);
                                send(new Request(0, response.encode(), null));
                            }

                            public void onFailure(Throwable value) {
                            }
                        };
                        // Looks like a dup delivery.. filter it out.
                        if( processed.get(publish.messageId())!=null ) {
                            return;
                        }
                        break;
                    case AT_MOST_ONCE:
                }
                listener.onPublish(publish.topicName(), publish.payload(), cb);
            } catch (Throwable e) {
                handleFatalFailure(e);
            }
        }
    }

    private void handleFatalFailure(Throwable error) {
        if( failure == null ) {
            failure = error;
            
            mqtt.tracer.debug("Fatal connection failure: %s", error);
            // Fail incomplete requests.
            ArrayList<Request> values = new ArrayList(requests.values());
            requests.clear();
            for (Request value : values) {
                if( value.cb!= null ) {
                    value.cb.onFailure(failure);
                }
            }

            ArrayList<Request> overflowEntries = new ArrayList<Request>(overflow);
            overflow.clear();
            for (Request entry : overflowEntries) {
                if( entry.cb !=null ) {
                    entry.cb.onFailure(failure);
                }
            }
            
            if( listener !=null && !disconnected ) {
                try {
                    listener.onFailure(failure);
                } catch (Exception e) {
                    Thread.currentThread().getUncaughtExceptionHandler().uncaughtException(Thread.currentThread(), e);
                }
            }
        }
    }

    private static IllegalStateException createListenerNotSetError() {
        return (IllegalStateException) new IllegalStateException("No connection listener set to handle message received from the server.").fillInStackTrace();
    }

    private static IllegalStateException createDisconnectedError() {
        return (IllegalStateException) new IllegalStateException("Disconnected").fillInStackTrace();
    }

    static private  String hex(SocketAddress address) {
        if( address instanceof InetSocketAddress ) {
            InetSocketAddress isa = (InetSocketAddress)address;
            return HexSupport.toHexFromBuffer(new Buffer(isa.getAddress().getAddress()))+Integer.toHexString(isa.getPort());
        }
        return "";
    }

}
