//
// ZYJMQTTSession.m
// ZYJMQTTClient.framework
//
// Copyright © 2013-2017, Christoph Krey. All rights reserved.
//


#import "ZYJMQTTSession.h"
#import "ZYJMQTTDecoder.h"
#import "ZYJMQTTStrict.h"
#import "ZYJMQTTProperties.h"
#import "ZYJMQTTMessage.h"
#import "ZYJMQTTCoreDataPersistence.h"
#import "ZYJGCDTimer.h"

@class ZYJMQTTSSLSecurityPolicy;

#import "ZYJMQTTLog.h"

NSString * const ZYJMQTTSessionErrorDomain = @"ZYJMQTT";

@interface ZYJMQTTSession() <ZYJMQTTDecoderDelegate, ZYJMQTTTransportDelegate>

@property (nonatomic, readwrite) ZYJMQTTSessionStatus status;
@property (nonatomic, readwrite) BOOL sessionPresent;

@property (strong, nonatomic) ZYJGCDTimer *keepAliveTimer;
@property (strong, nonatomic) NSNumber *serverKeepAlive;
@property (nonatomic) UInt16 effectiveKeepAlive;
@property (strong, nonatomic) ZYJGCDTimer *checkDupTimer;

@property (strong, nonatomic) ZYJMQTTDecoder *decoder;

@property (copy, nonatomic) ZYJMQTTDisconnectHandler disconnectHandler;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, ZYJMQTTSubscribeHandler> *subscribeHandlers;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, ZYJMQTTUnsubscribeHandler> *unsubscribeHandlers;
@property (nonatomic, strong) NSMutableDictionary<NSNumber *, ZYJMQTTPublishHandler> *publishHandlers;

@property (nonatomic) UInt16 txMsgId;

@property (nonatomic) BOOL synchronPub;
@property (nonatomic) UInt16 synchronPubMid;
@property (nonatomic) BOOL synchronUnsub;
@property (nonatomic) UInt16 synchronUnsubMid;
@property (nonatomic) BOOL synchronSub;
@property (nonatomic) UInt16 synchronSubMid;
@property (nonatomic) BOOL synchronConnect;
@property (nonatomic) BOOL synchronDisconnect;

@property (strong, nonatomic) ZYJMQTTSSLSecurityPolicy *securityPolicy;

@end

#define DUPLOOP 1.0

@implementation ZYJMQTTSession
@synthesize certificates;

- (void)setCertificates:(NSArray *)newCertificates {
    certificates = newCertificates;
    if (self.transport) {
        if ([self.transport respondsToSelector:@selector(setCertificates:)]) {
            [self.transport performSelector:@selector(setCertificates:) withObject:certificates];
        }
    }
}

- (instancetype)init {
    NSLog(@"[ZYJMQTTSession] init");
    self = [super init];
    self.txMsgId = 1;
    self.persistence = [[ZYJMQTTCoreDataPersistence alloc] init];
    self.subscribeHandlers = [[NSMutableDictionary alloc] init];
    self.unsubscribeHandlers = [[NSMutableDictionary alloc] init];
    self.publishHandlers = [[NSMutableDictionary alloc] init];

    self.clientId = nil;
    self.userName = nil;
    self.password = nil;
    self.keepAliveInterval = 60;
    self.dupTimeout = 20.0;
    self.cleanSessionFlag = true;
    self.willFlag = false;
    self.willTopic = nil;
    self.willMsg = nil;
    self.willQoS = ZYJMQTTQosLevelAtMostOnce;
    self.willRetainFlag = false;
    self.protocolLevel = ZYJMQTTProtocolVersion311;
    self.queue = dispatch_get_main_queue();
    self.status = ZYJMQTTSessionStatusCreated;
    self.streamSSLLevel = (NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL;
    return self;
}

- (void)dealloc {
    [self.keepAliveTimer invalidate];
    [self.checkDupTimer invalidate];
}

- (NSString *)host {
    return _transport.host;
}

- (UInt32)port {
    return _transport.port;
}

- (void)setClientId:(NSString *)clientId {
    if (!clientId) {
        clientId = [NSString stringWithFormat:@"ZYJMQTTClient%.0f",fmod([NSDate date].timeIntervalSince1970, 1.0) * 1000000.0];
    }

    _clientId = clientId;
}

- (void)setStreamSSLLevel:(NSString *)streamSSLLevel {
    _streamSSLLevel = streamSSLLevel;
    self.transport.streamSSLLevel = self.streamSSLLevel;
}

- (UInt16)subscribeToTopic:(NSString *)topic
                   atLevel:(ZYJMQTTQosLevel)qosLevel {
    return [self subscribeToTopic:topic atLevel:qosLevel subscribeHandler:nil];
}

- (UInt16)subscribeToTopic:(NSString *)topic
                   atLevel:(ZYJMQTTQosLevel)qosLevel
          subscribeHandler:(ZYJMQTTSubscribeHandler)subscribeHandler {
    return [self subscribeToTopics:topic ? @{topic: @(qosLevel)} : @{} subscribeHandler:subscribeHandler];
}

- (UInt16)subscribeToTopics:(NSDictionary<NSString *, NSNumber *> *)topics {
    return [self subscribeToTopics:topics subscribeHandler:nil];
}

- (void)checkTopicFilters:(NSArray <NSString *> *)topicFilters {
    if (ZYJMQTTStrict.strict &&
        topicFilters.count == 0) {
        NSException* myException = [NSException
                                    exceptionWithName:@"topicFilter array in SUBSCRIBE or UNSUBSRIBE must not be empty"
                                    reason:[NSString stringWithFormat:@"%@", topicFilters]
                                    userInfo:nil];
        @throw myException;
    }

    for (NSString *topicFilter in topicFilters) {
        if (ZYJMQTTStrict.strict &&
            topicFilter.length < 1) {
            NSException* myException = [NSException
                                        exceptionWithName:@"topicFilter must be at least 1 characters long"
                                        reason:[NSString stringWithFormat:@"%@", topicFilter]
                                        userInfo:nil];
            @throw myException;
        }

        if (ZYJMQTTStrict.strict &&
            [topicFilter dataUsingEncoding:NSUTF8StringEncoding].length > 65535L) {
            NSException* myException = [NSException
                                        exceptionWithName:@"topicFilter may not be longer than 65535 bytes in UTF8 representation"
                                        reason:[NSString stringWithFormat:@"topicFilter length = %lu",
                                                (unsigned long)[topicFilter dataUsingEncoding:NSUTF8StringEncoding].length]
                                        userInfo:nil];
            @throw myException;
        }

        if (ZYJMQTTStrict.strict &&
            ![topicFilter dataUsingEncoding:NSUTF8StringEncoding]) {
            NSException* myException = [NSException
                                        exceptionWithName:@"topicFilter must not contain non-UTF8 characters"
                                        reason:[NSString stringWithFormat:@"topicFilter = %@", topicFilter]
                                        userInfo:nil];
            @throw myException;
        }

        if (ZYJMQTTStrict.strict) {
            NSArray <NSString *> *components = [topicFilter componentsSeparatedByString:@"/"];
            for (int level = 0; level < components.count; level++) {
                if ([components[level] rangeOfString:@"+"].location != NSNotFound &&
                    components[level].length > 1) {
                    NSException* myException = [NSException
                                                exceptionWithName:@"singlelevel wildcard must be alone on a level of a topic filter"
                                                reason:[NSString stringWithFormat:@"topicFilter = %@", topicFilter]
                                                userInfo:nil];
                    @throw myException;
                }
            }

            for (int level = 0; level < components.count - 1; level++) {
                if ([components[level] rangeOfString:@"#"].location != NSNotFound) {
                    NSException* myException = [NSException
                                                exceptionWithName:@"multilevel wildcard must be on the last level of a topic filter"
                                                reason:[NSString stringWithFormat:@"topicFilter = %@", topicFilter]
                                                userInfo:nil];
                    @throw myException;
                }
            }
            if ([components[components.count - 1] rangeOfString:@"#"].location != NSNotFound &&
                components[components.count - 1].length > 1) {
                NSException* myException = [NSException
                                            exceptionWithName:@"multilevel wildcard must be alone on a level of a topic filter"
                                            reason:[NSString stringWithFormat:@"topicFilter = %@", topicFilter]
                                            userInfo:nil];
                @throw myException;
            }
        }

        if (ZYJMQTTStrict.strict &&
            [topicFilter rangeOfString:@"#"].location != NSNotFound &&
            [topicFilter rangeOfString:@"#"].location != topicFilter.length &&
            (topicFilter.length == 1 || [[topicFilter substringWithRange:NSMakeRange(topicFilter.length - 2, 1)] isEqualToString:@"/"])
            ) {
            NSException* myException = [NSException
                                        exceptionWithName:@"multilevel wildcard must alone on the last level of a topic filter"
                                        reason:[NSString stringWithFormat:@"topicFilter = %@", topicFilter]
                                        userInfo:nil];
            @throw myException;
        }

    }
}

- (UInt16)subscribeToTopics:(NSDictionary<NSString *, NSNumber *> *)topics subscribeHandler:(ZYJMQTTSubscribeHandler)subscribeHandler {
//    NSLog(@"[ZYJMQTTSession] subscribeToTopics:%@]", topics);

    [self checkTopicFilters:topics.allKeys];

    for (NSNumber *qos in topics.allValues) {
        if (ZYJMQTTStrict.strict &&
            qos.intValue != ZYJMQTTQosLevelAtMostOnce &&
            qos.intValue != ZYJMQTTQosLevelAtLeastOnce &&
            qos.intValue != ZYJMQTTQosLevelExactlyOnce) {
            NSException* myException = [NSException
                                        exceptionWithName:@"Illegal QoS level"
                                        reason:[NSString stringWithFormat:@"%d is not 0, 1, or 2", qos.intValue]
                                        userInfo:nil];
            @throw myException;
        }
    }

    UInt16 mid = [self nextMsgId];
    if (subscribeHandler) {
        (self.subscribeHandlers)[@(mid)] = [subscribeHandler copy];
    } else {
        [self.subscribeHandlers removeObjectForKey:@(mid)];
    }
    (void)[self encode:[ZYJMQTTMessage subscribeMessageWithMessageId:mid
                                                           topics:topics
                                                    protocolLevel:self.protocolLevel
                                           subscriptionIdentifier:nil]];

    return mid;
}

- (UInt16)unsubscribeTopic:(NSString*)topic {
    return [self unsubscribeTopic:topic unsubscribeHandler:nil];
}

- (UInt16)unsubscribeTopic:(NSString *)topic unsubscribeHandler:(ZYJMQTTUnsubscribeHandler)unsubscribeHandler {
    return [self unsubscribeTopics:topic ? @[topic] : @[] unsubscribeHandler:unsubscribeHandler];
}

- (UInt16)unsubscribeTopics:(NSArray<NSString *> *)topics {
    return [self unsubscribeTopics:topics unsubscribeHandler:nil];
}

- (UInt16)unsubscribeTopics:(NSArray<NSString *> *)topics unsubscribeHandler:(ZYJMQTTUnsubscribeHandler)unsubscribeHandler {
//    NSLog(@"[ZYJMQTTSession] unsubscribeTopics:%@", topics);

    [self checkTopicFilters:topics];

    UInt16 mid = [self nextMsgId];
    if (unsubscribeHandler) {
        (self.unsubscribeHandlers)[@(mid)] = [unsubscribeHandler copy];
    } else {
        [self.unsubscribeHandlers removeObjectForKey:@(mid)];
    }
    (void)[self encode:[ZYJMQTTMessage unsubscribeMessageWithMessageId:mid
                                                             topics:topics
                                                      protocolLevel:self.protocolLevel]];
    return mid;
}

- (UInt16)publishData:(NSData*)data
              onTopic:(NSString*)topic
               retain:(BOOL)retainFlag
                  qos:(ZYJMQTTQosLevel)qos {
    return [self publishData:data onTopic:topic retain:retainFlag qos:qos publishHandler:nil];
}

- (UInt16)publishData:(NSData *)data
              onTopic:(NSString *)topic
               retain:(BOOL)retainFlag
                  qos:(ZYJMQTTQosLevel)qos
       publishHandler:(ZYJMQTTPublishHandler)publishHandler
{
//    NSLog(@"[ZYJMQTTSession] publishData:%@... onTopic:%@ retain:%d qos:%ld publishHandler:%p",
//                 [data subdataWithRange:NSMakeRange(0, MIN(256, data.length))],
//                 [topic substringWithRange:NSMakeRange(0, MIN(256, topic.length))],
//                 retainFlag,
//                 (long)qos,
//                 publishHandler);

    if (ZYJMQTTStrict.strict &&
        !topic) {
        NSException* myException = [NSException
                                    exceptionWithName:@"topic must not be nil"
                                    reason:[NSString stringWithFormat:@"%@", topic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        topic &&
        topic.length < 1) {
        NSException* myException = [NSException
                                    exceptionWithName:@"topic must not at least 1 character long"
                                    reason:[NSString stringWithFormat:@"%@", topic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        topic &&
        [topic dataUsingEncoding:NSUTF8StringEncoding].length > 65535L) {
        NSException* myException = [NSException
                                    exceptionWithName:@"topic may not be longer than 65535 bytes in UTF8 representation"
                                    reason:[NSString stringWithFormat:@"topic length = %lu",
                                            (unsigned long)[topic dataUsingEncoding:NSUTF8StringEncoding].length]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        topic &&
        ![topic dataUsingEncoding:NSUTF8StringEncoding]) {
        NSException* myException = [NSException
                                    exceptionWithName:@"topic must not contain non-UTF8 characters"
                                    reason:[NSString stringWithFormat:@"topic = %@", topic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.willTopic &&
        ([self.willTopic containsString:@"+"] ||
         [self.willTopic containsString:@"#"])
        ) {
        NSException* myException = [NSException
                                    exceptionWithName:@"willTopic must not contain wildcards"
                                    reason:[NSString stringWithFormat:@"willTopic = %@", self.willTopic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        qos != ZYJMQTTQosLevelAtMostOnce &&
        qos != ZYJMQTTQosLevelAtLeastOnce &&
        qos != ZYJMQTTQosLevelExactlyOnce) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Illegal QoS level"
                                    reason:[NSString stringWithFormat:@"%d is not 0, 1, or 2", qos]
                                    userInfo:nil];
        @throw myException;
    }

    UInt16 msgId = 0;
    if (!qos) {
        ZYJMQTTMessage *msg = [ZYJMQTTMessage publishMessageWithData:data
                                                       onTopic:topic
                                                           qos:qos
                                                         msgId:msgId
                                                    retainFlag:retainFlag
                                                       dupFlag:FALSE
                                                 protocolLevel:self.protocolLevel
                                        payloadFormatIndicator:nil
                                     publicationExpiryInterval:nil
                                                    topicAlias:nil
                                                 responseTopic:nil
                                               correlationData:nil
                                                  userProperty:nil
                                                   contentType:nil];
        NSError *error = nil;
        if (![self encode:msg]) {
            error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                        code:ZYJMQTTSessionErrorEncoderNotReady
                                    userInfo:@{NSLocalizedDescriptionKey : @"Encoder not ready"}];
        }
        if (publishHandler) {
            [self onPublish:publishHandler error:error];
        }
    } else {
        msgId = [self nextMsgId];
        ZYJMQTTMessage *msg = nil;

        id<ZYJMQTTFlow> flow;
        if (self.status == ZYJMQTTSessionStatusConnected) {
            NSArray *flows = [self.persistence allFlowsforClientId:self.clientId
                                                      incomingFlag:NO];

            BOOL unprocessedMessageNotExists = TRUE;
            NSUInteger windowSize = 0;
            for (id<ZYJMQTTFlow> flow in flows) {
                if ((flow.commandType).intValue != ZYJMQTT_None) {
                    windowSize++;
                } else {
                    unprocessedMessageNotExists = FALSE;
                }
            }
            if (unprocessedMessageNotExists && windowSize <= self.persistence.maxWindowSize) {
                msg = [ZYJMQTTMessage publishMessageWithData:data
                                                  onTopic:topic
                                                      qos:qos
                                                    msgId:msgId
                                               retainFlag:retainFlag
                                                  dupFlag:FALSE
                                            protocolLevel:self.protocolLevel
                                   payloadFormatIndicator:nil
                                publicationExpiryInterval:nil
                                               topicAlias:nil
                                            responseTopic:nil
                                          correlationData:nil
                                             userProperty:nil
                                              contentType:nil];
                flow = [self.persistence storeMessageForClientId:self.clientId
                                                           topic:topic
                                                            data:data
                                                      retainFlag:retainFlag
                                                             qos:qos
                                                           msgId:msgId
                                                    incomingFlag:NO
                                                     commandType:ZYJMQTTPublish
                                                        deadline:[NSDate dateWithTimeIntervalSinceNow:self.dupTimeout]];
            }
        }
        if (!msg) {
            flow = [self.persistence storeMessageForClientId:self.clientId
                                                       topic:topic
                                                        data:data
                                                  retainFlag:retainFlag
                                                         qos:qos
                                                       msgId:msgId
                                                incomingFlag:NO
                                                 commandType:ZYJMQTT_None
                                                    deadline:[NSDate date]];
        }
        if (!flow) {
//            NSLog(@"[ZYJMQTTSession] dropping outgoing message %d", msgId);
            NSError *error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                                 code:ZYJMQTTSessionErrorDroppingOutgoingMessage
                                             userInfo:@{NSLocalizedDescriptionKey : @"Dropping outgoing Message"}];
            if (publishHandler) {
                [self onPublish:publishHandler error:error];
            }
            msgId = 0;
        } else {
            [self.persistence sync];
            if (publishHandler) {
                (self.publishHandlers)[@(msgId)] = [publishHandler copy];
            } else {
                [self.publishHandlers removeObjectForKey:@(msgId)];
            }

            if ((flow.commandType).intValue == ZYJMQTTPublish) {
//                NSLog(@"[ZYJMQTTSession] PUBLISH %d", msgId);
                if (![self encode:msg]) {
//                    NSLog(@"[ZYJMQTTSession] queueing message %d after unsuccessfull attempt", msgId);
                    flow.commandType = [NSNumber numberWithUnsignedInt:ZYJMQTT_None];
                    flow.deadline = [NSDate date];
                    [self.persistence sync];
                }
            } else {
//                NSLog(@"[ZYJMQTTSession] queueing message %d", msgId);
            }
        }
    }
    [self tell];
    return msgId;
}

- (void)closeWithDisconnectHandler:(ZYJMQTTDisconnectHandler)disconnectHandler {
    [self closeWithReturnCode:ZYJMQTTSuccess
        sessionExpiryInterval:nil
                 reasonString:nil
                 userProperty:nil
            disconnectHandler:disconnectHandler];
}

- (void)closeWithReturnCode:(ZYJMQTTReturnCode)returnCode
      sessionExpiryInterval:(NSNumber *)sessionExpiryInterval
               reasonString:(NSString *)reasonString
               userProperty:(NSDictionary<NSString *,NSString *> *)userProperty
          disconnectHandler:(ZYJMQTTDisconnectHandler)disconnectHandler {
    NSLog(@"[ZYJMQTTSession] closeWithDisconnectHandler:%p ", disconnectHandler);
    self.disconnectHandler = disconnectHandler;

    if (self.status == ZYJMQTTSessionStatusConnected) {
        [self disconnectWithReturnCode:returnCode
                 sessionExpiryInterval:sessionExpiryInterval
                          reasonString:reasonString
                          userProperty:userProperty];
    } else {
        [self closeInternal];
    }
}

- (void)disconnect {
    [self disconnectWithReturnCode:ZYJMQTTSuccess
             sessionExpiryInterval:nil
                      reasonString:nil
                      userProperty:nil];
}

- (void)disconnectWithReturnCode:(ZYJMQTTReturnCode)returnCode
           sessionExpiryInterval:(NSNumber *)sessionExpiryInterval
                    reasonString:(NSString *)reasonString
                    userProperty:(NSDictionary<NSString *,NSString *> *)userProperty {
    NSLog(@"[ZYJMQTTSession] sending DISCONNECT");
    self.status = ZYJMQTTSessionStatusDisconnecting;

    [self encode:[ZYJMQTTMessage disconnectMessage:self.protocolLevel
                                     returnCode:returnCode
                          sessionExpiryInterval:sessionExpiryInterval
                                   reasonString:reasonString
                                   userProperty:userProperty]];
    [self closeInternal];
}

- (void)closeInternal {
    NSLog(@"[ZYJMQTTSession] closeInternal");

    if (self.checkDupTimer) {
        [self.checkDupTimer invalidate];
        self.checkDupTimer = nil;
    }

    if (self.keepAliveTimer) {
        [self.keepAliveTimer invalidate];
        self.keepAliveTimer = nil;
    }

    if (self.transport) {
        [self.transport close];
        self.transport.delegate = nil;
    }

    if(self.decoder){
        [self.decoder close];
        self.decoder.delegate = nil;
    }

    NSArray *flows = [self.persistence allFlowsforClientId:self.clientId
                                              incomingFlag:NO];
    for (id<ZYJMQTTFlow> flow in flows) {
        switch ((flow.commandType).intValue) {
            case ZYJMQTTPublish:
            case ZYJMQTTPubrel:
                flow.deadline = [flow.deadline dateByAddingTimeInterval:-self.dupTimeout];
                [self.persistence sync];
                break;
        }
    }

    self.status = ZYJMQTTSessionStatusClosed;
    if ([self.delegate respondsToSelector:@selector(handleEvent:event:error:)]) {
        [self.delegate handleEvent:self event:ZYJMQTTSessionEventConnectionClosed error:nil];
    }
    if ([self.delegate respondsToSelector:@selector(connectionClosed:)]) {
        [self.delegate connectionClosed:self];
    }

    NSError *error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                         code:ZYJMQTTSessionErrorNoResponse
                                     userInfo:@{NSLocalizedDescriptionKey : @"No response"}];

    NSArray *allSubscribeHandlers = self.subscribeHandlers.allValues;
    [self.subscribeHandlers removeAllObjects];
    for (ZYJMQTTSubscribeHandler subscribeHandler in allSubscribeHandlers) {
        subscribeHandler(error, nil);
    }

    NSArray *allUnsubscribeHandlers = self.unsubscribeHandlers.allValues;
    [self.unsubscribeHandlers removeAllObjects];
    for (ZYJMQTTUnsubscribeHandler unsubscribeHandler in allUnsubscribeHandlers) {
        unsubscribeHandler(error);
    }

    ZYJMQTTDisconnectHandler disconnectHandler = self.disconnectHandler;
    if (disconnectHandler) {
        self.disconnectHandler = nil;
        disconnectHandler(nil);
    }

    [self tell];
    self.synchronPub = FALSE;
    self.synchronPubMid = 0;
    self.synchronSub = FALSE;
    self.synchronSubMid = 0;
    self.synchronUnsub = FALSE;
    self.synchronUnsubMid = 0;
}


- (void)keepAlive {
//    NSLog(@"[ZYJMQTTSession] keepAlive %@ @%.0f", self.clientId, [[NSDate date] timeIntervalSince1970]);
    (void)[self encode:[ZYJMQTTMessage pingreqMessage]];
}

- (void)checkDup {
//    NSLog(@"[ZYJMQTTSession] checkDup %@ @%.0f", self.clientId, [[NSDate date] timeIntervalSince1970]);
    [self checkTxFlows];
}

- (void)checkTxFlows {
    NSUInteger windowSize;
    ZYJMQTTMessage *message;
    if (self.status != ZYJMQTTSessionStatusConnected) {
        return;
    }

    NSArray *flows = [self.persistence allFlowsforClientId:self.clientId
                                              incomingFlag:NO];
    windowSize = 0;
    message = nil;

    for (id<ZYJMQTTFlow> flow in flows) {
        if ((flow.commandType).intValue != ZYJMQTT_None) {
            windowSize++;
        }
    }
    for (id<ZYJMQTTFlow> flow in flows) {
//        NSLog(@"[ZYJMQTTSession] %@ flow %@ %@ %@", self.clientId, flow.deadline, flow.commandType, flow.messageId);
        if ([flow.deadline compare:[NSDate date]] == NSOrderedAscending) {
            switch ((flow.commandType).intValue) {
                case 0:
                    if (windowSize <= self.persistence.maxWindowSize) {
//                        NSLog(@"[ZYJMQTTSession] PUBLISH queued message %@", flow.messageId);
                        message = [ZYJMQTTMessage publishMessageWithData:flow.data
                                                              onTopic:flow.topic
                                                                  qos:(flow.qosLevel).intValue
                                                                msgId:(flow.messageId).intValue
                                                           retainFlag:(flow.retainedFlag).boolValue
                                                              dupFlag:NO
                                                        protocolLevel:self.protocolLevel
                                               payloadFormatIndicator:nil
                                            publicationExpiryInterval:nil
                                                           topicAlias:nil
                                                        responseTopic:nil
                                                      correlationData:nil
                                                         userProperty:nil
                                                          contentType:nil];
                        if ([self encode:message]) {
                            flow.commandType = @(ZYJMQTTPublish);
                            flow.deadline = [NSDate dateWithTimeIntervalSinceNow:self.dupTimeout];
                            [self.persistence sync];
                            windowSize++;
                        }
                    }
                    break;
                case ZYJMQTTPublish:
//                    NSLog(@"[ZYJMQTTSession] resend PUBLISH %@", flow.messageId);
                    message = [ZYJMQTTMessage publishMessageWithData:flow.data
                                                          onTopic:flow.topic
                                                              qos:(flow.qosLevel).intValue
                                                            msgId:(flow.messageId).intValue
                                                       retainFlag:(flow.retainedFlag).boolValue
                                                          dupFlag:YES
                                                    protocolLevel:self.protocolLevel
                                           payloadFormatIndicator:nil
                                        publicationExpiryInterval:nil
                                                       topicAlias:nil
                                                    responseTopic:nil
                                                  correlationData:nil
                                                     userProperty:nil
                                                      contentType:nil];
                    if ([self encode:message]) {
                        flow.deadline = [NSDate dateWithTimeIntervalSinceNow:self.dupTimeout];
                        [self.persistence sync];
                    }
                    break;
                case ZYJMQTTPubrel:
//                    NSLog(@"[ZYJMQTTSession] resend PUBREL %@", flow.messageId);
                    message = [ZYJMQTTMessage pubrelMessageWithMessageId:(flow.messageId).intValue
                                                        protocolLevel:self.protocolLevel
                                                           returnCode:ZYJMQTTSuccess
                                                         reasonString:nil
                                                         userProperty:nil];
                    if ([self encode:message]) {
                        flow.deadline = [NSDate dateWithTimeIntervalSinceNow:self.dupTimeout];
                        [self.persistence sync];
                    }
                    break;
                default:
                    break;
            }
        }
    }
}

- (void)decoder:(ZYJMQTTDecoder *)sender handleEvent:(ZYJMQTTDecoderEvent)eventCode error:(NSError *)error {
    __unused NSArray *events = @[
                                 @"ZYJMQTTDecoderEventProtocolError",
                                 @"ZYJMQTTDecoderEventConnectionClosed",
                                 @"ZYJMQTTDecoderEventConnectionError"
                                 ];
//    NSLog(@"[ZYJMQTTSession] decoder handleEvent: %@ (%d) %@",
//                 events[eventCode % [events count]],
//                 eventCode,
//                 [error description]);

    switch (eventCode) {
        case ZYJMQTTDecoderEventConnectionClosed:
            [self error:ZYJMQTTSessionEventConnectionClosedByBroker error:error];
            break;
        case ZYJMQTTDecoderEventConnectionError:
            [self connectionError:error];
            break;
        case ZYJMQTTDecoderEventProtocolError:
            [self protocolError:error];
            break;
    }
    ZYJMQTTConnectHandler connectHandler = self.connectHandler;
    if (connectHandler) {
        self.connectHandler = nil;
        [self onConnect:connectHandler error:error];
    }
}

- (void)decoder:(ZYJMQTTDecoder *)sender didReceiveMessage:(NSData *)data {
    ZYJMQTTMessage *message = [ZYJMQTTMessage messageFromData:data protocolLevel:self.protocolLevel];
    if (!message) {
//        NSLog(@"[ZYJMQTTSession] ZYJMQTT illegal message received");
        NSError * error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                              code:ZYJMQTTSessionErrorIllegalMessageReceived
                                          userInfo:@{NSLocalizedDescriptionKey : @"ZYJMQTT illegal message received"}];
        [self protocolError:error];

        return;
    }

    @synchronized(sender) {
        if ([self.delegate respondsToSelector:@selector(received:type:qos:retained:duped:mid:data:)]) {
            [self.delegate received:self
                               type:message.type
                                qos:message.qos
                           retained:message.retainFlag
                              duped:message.dupFlag
                                mid:message.mid
                               data:message.data];
        }
        if ([self.delegate respondsToSelector:@selector(ignoreReceived:type:qos:retained:duped:mid:data:)]) {
            if ([self.delegate ignoreReceived:self
                                         type:message.type
                                          qos:message.qos
                                     retained:message.retainFlag
                                        duped:message.dupFlag
                                          mid:message.mid
                                         data:message.data]) {
                return;
            }
        }
        switch (self.status) {
            case ZYJMQTTSessionStatusConnecting:
                switch (message.type) {
                    case ZYJMQTTConnack:
                        if ((self.protocolLevel == ZYJMQTTProtocolVersion50 && message.data.length < 3) ||
                            (self.protocolLevel != ZYJMQTTProtocolVersion50 && message.data.length != 2)) {
                            NSError *error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                                                 code:ZYJMQTTSessionErrorInvalidConnackReceived
                                                             userInfo:@{NSLocalizedDescriptionKey : @"ZYJMQTT protocol CONNACK expected"}];

                            [self protocolError:error];
                            ZYJMQTTConnectHandler connectHandler = self.connectHandler;
                            if (connectHandler) {
                                self.connectHandler = nil;
                                [self onConnect:connectHandler error:error];
                            }
                        } else {
                            if (message.returnCode && (message.returnCode).intValue == ZYJMQTTSuccess) {
                                self.status = ZYJMQTTSessionStatusConnected;
                                if (message.connectAcknowledgeFlags &&
                                    ((message.connectAcknowledgeFlags).unsignedIntValue & 0x01) == 0x01) {
                                    self.sessionPresent = true;
                                } else {
                                    self.sessionPresent = false;
                                }
                                __weak typeof(self) weakSelf = self;
                                self.checkDupTimer = [ZYJGCDTimer scheduledTimerWithTimeInterval:DUPLOOP
                                                                                      repeats:YES
                                                                                        queue:self.queue
                                                                                        block:^{
                                                                                            [weakSelf checkDup];
                                                                                        }];
                                [self checkDup];

                                if (message.properties) {
                                    self.serverKeepAlive = message.properties.serverKeepAlive;
                                }
                                if (self.serverKeepAlive) {
                                    self.effectiveKeepAlive = (self.serverKeepAlive).unsignedShortValue;
                                } else {
                                    self.effectiveKeepAlive = self.keepAliveInterval;
                                }

                                if (self.effectiveKeepAlive > 0) {
                                    self.keepAliveTimer = [ZYJGCDTimer scheduledTimerWithTimeInterval:self.effectiveKeepAlive
                                                                                           repeats:YES
                                                                                             queue: self.queue
                                                                                             block:^() {
                                                                                                 [weakSelf keepAlive];
                                                                                             }];
                                }

                                if ([self.delegate respondsToSelector:@selector(handleEvent:event:error:)]) {
                                    [self.delegate handleEvent:self event:ZYJMQTTSessionEventConnected error:nil];
                                }
                                if ([self.delegate respondsToSelector:@selector(connected:)]) {
                                    [self.delegate connected:self];
                                }
                                if ([self.delegate respondsToSelector:@selector(connected:sessionPresent:)]) {
                                    [self.delegate connected:self sessionPresent:self.sessionPresent];
                                }

                                if (self.connectionHandler) {
                                    self.connectionHandler(ZYJMQTTSessionEventConnected);
                                }
                                ZYJMQTTConnectHandler connectHandler = self.connectHandler;
                                if (connectHandler) {
                                    self.connectHandler = nil;
                                    [self onConnect:connectHandler error:nil];
                                }

                            } else {
                                NSString *errorDescription = @"unknown";
                                NSInteger errorCode = 0;
                                if (message.returnCode) {
                                    switch ((message.returnCode).intValue) {
                                        case 1:
                                            errorDescription = @"ZYJMQTT CONNACK: unacceptable protocol version";
                                            errorCode = ZYJMQTTSessionErrorConnackUnacceptableProtocolVersion;
                                            break;
                                        case 2:
                                            errorDescription = @"ZYJMQTT CONNACK: identifier rejected";
                                            errorCode = ZYJMQTTSessionErrorConnackIdentifierRejected;
                                            break;
                                        case 3:
                                            errorDescription = @"ZYJMQTT CONNACK: server unavailable";
                                            errorCode = ZYJMQTTSessionErrorConnackServeUnavailable;
                                            break;
                                        case 4:
                                            errorDescription = @"ZYJMQTT CONNACK: bad user name or password";
                                            errorCode = ZYJMQTTSessionErrorConnackBadUsernameOrPassword;
                                            break;
                                        case 5:
                                            errorDescription = @"ZYJMQTT CONNACK: not authorized";
                                            errorCode = ZYJMQTTSessionErrorConnackNotAuthorized;
                                            break;
                                        default:
                                            errorDescription = @"ZYJMQTT CONNACK: reserved for future use";
                                            errorCode = ZYJMQTTSessionErrorConnackReserved;
                                            break;
                                    }
                                }

                                NSError *error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                                                     code:errorCode
                                                                 userInfo:@{NSLocalizedDescriptionKey : errorDescription}];
                                [self error:ZYJMQTTSessionEventConnectionRefused error:error];
                                if ([self.delegate respondsToSelector:@selector(connectionRefused:error:)]) {
                                    [self.delegate connectionRefused:self error:error];
                                }
                                ZYJMQTTConnectHandler connectHandler = self.connectHandler;
                                if (connectHandler) {
                                    self.connectHandler = nil;
                                    [self onConnect:connectHandler error:error];
                                }
                            }

                            self.synchronConnect = FALSE;
                        }
                        break;
                    case ZYJMQTTDisconnect: {
                        NSError *error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                                             code:(message.returnCode).intValue
                                                         userInfo:@{NSLocalizedDescriptionKey : @"ZYJMQTT protocol DISCONNECT instead of CONNACK"}];

                        [self protocolError:error];
                        ZYJMQTTConnectHandler connectHandler = self.connectHandler;
                        if (connectHandler) {
                            self.connectHandler = nil;
                            [self onConnect:connectHandler error:error];
                        }
                        break;
                    }
                    default: {
                        NSError * error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                                              code:ZYJMQTTSessionErrorNoConnackReceived
                                                          userInfo:@{NSLocalizedDescriptionKey : @"ZYJMQTT protocol no CONNACK"}];
                        [self protocolError:error];
                        ZYJMQTTConnectHandler connectHandler = self.connectHandler;
                        if (connectHandler) {
                            self.connectHandler = nil;
                            [self onConnect:connectHandler error:error];
                        }
                        break;
                    }
                }
                break;
            case ZYJMQTTSessionStatusConnected:
                switch (message.type) {
                    case ZYJMQTTPublish:
                        [self handlePublish:message];
                        break;
                    case ZYJMQTTPuback:
                        [self handlePuback:message];
                        break;
                    case ZYJMQTTPubrec:
                        [self handlePubrec:message];
                        break;
                    case ZYJMQTTPubrel:
                        [self handlePubrel:message];
                        break;
                    case ZYJMQTTPubcomp:
                        [self handlePubcomp:message];
                        break;
                    case ZYJMQTTSuback:
                        [self handleSuback:message];
                        break;
                    case ZYJMQTTUnsuback:
                        [self handleUnsuback:message];
                        break;
                    case ZYJMQTTDisconnect: {
                        NSError *error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                                             code:(message.returnCode).intValue
                                                         userInfo:@{NSLocalizedDescriptionKey : @"ZYJMQTT protocol DISCONNECT received"}];

                        [self protocolError:error];
                    }

                    default:
                        break;
                }
                break;
            default:
                NSLog(@"[ZYJMQTTSession] other state");
                break;
        }
    }
}

- (void)handlePublish:(ZYJMQTTMessage*)msg {
    NSData *data = msg.data;
    if (data.length < 2) {
        return;
    }
    UInt8 const *bytes = data.bytes;
    UInt16 topicLength = 256 * bytes[0] + bytes[1];
    if (data.length < 2 + topicLength) {
        return;
    }
    NSData *topicData = [data subdataWithRange:NSMakeRange(2, topicLength)];
    NSString *topic = [[NSString alloc] initWithData:topicData
                                            encoding:NSUTF8StringEncoding];
    if (!topic) {
        topic = [[NSString alloc] initWithData:topicData
                                      encoding:NSISOLatin1StringEncoding];
        NSLog(@"non UTF8 topic %@", topic);
    }
    NSRange range = NSMakeRange(2 + topicLength, data.length - topicLength - 2);
    data = [data subdataWithRange:range];

    if (msg.qos == 0) {
        if (self.protocolLevel == ZYJMQTTProtocolVersion50) {
            int propertiesLength = [ZYJMQTTProperties getVariableLength:data];
            int variableLength = [ZYJMQTTProperties variableIntLength:propertiesLength];
            msg.properties = [[ZYJMQTTProperties alloc] initFromData:data];
            NSRange range = NSMakeRange(variableLength + propertiesLength, data.length - variableLength - propertiesLength);
            data = [data subdataWithRange:range];
        }
        if ([self.delegate respondsToSelector:@selector(newMessage:data:onTopic:qos:retained:mid:)]) {
            [self.delegate newMessage:self
                                 data:data
                              onTopic:topic
                                  qos:msg.qos
                             retained:msg.retainFlag
                                  mid:0];
        }
        if ([self.delegate respondsToSelector:@selector(newMessageWithFeedback:data:onTopic:qos:retained:mid:)]) {
            [self.delegate newMessageWithFeedback:self
                                             data:data
                                          onTopic:topic
                                              qos:msg.qos
                                         retained:msg.retainFlag
                                              mid:0];
        }
        if (self.messageHandler) {
            self.messageHandler(data, topic);
        }
    } else {
        if (data.length >= 2) {
            bytes = data.bytes;
            UInt16 msgId = 256 * bytes[0] + bytes[1];
            msg.mid = msgId;
            data = [data subdataWithRange:NSMakeRange(2, data.length - 2)];
            if (msg.qos == 1) {
                if (self.protocolLevel == ZYJMQTTProtocolVersion50) {
                    int propertiesLength = [ZYJMQTTProperties getVariableLength:data];
                    int variableLength = [ZYJMQTTProperties variableIntLength:propertiesLength];
                    msg.properties = [[ZYJMQTTProperties alloc] initFromData:data];
                    NSRange range = NSMakeRange(variableLength + propertiesLength, data.length - variableLength - propertiesLength);
                    data = [data subdataWithRange:range];
                }

                BOOL processed = true;
                if ([self.delegate respondsToSelector:@selector(newMessage:data:onTopic:qos:retained:mid:)]) {
                    [self.delegate newMessage:self
                                         data:data
                                      onTopic:topic
                                          qos:msg.qos
                                     retained:msg.retainFlag
                                          mid:msgId];
                }
                if ([self.delegate respondsToSelector:@selector(newMessageWithFeedback:data:onTopic:qos:retained:mid:)]) {
                    processed = [self.delegate newMessageWithFeedback:self
                                                                 data:data
                                                              onTopic:topic
                                                                  qos:msg.qos
                                                             retained:msg.retainFlag
                                                                  mid:msgId];
                }
                if (self.messageHandler) {
                    self.messageHandler(data, topic);
                }
                if (processed) {
                    (void)[self encode:[ZYJMQTTMessage pubackMessageWithMessageId:msgId
                                                                 protocolLevel:self.protocolLevel
                                                                    returnCode:ZYJMQTTSuccess
                                                                  reasonString:nil
                                                                  userProperty:nil]];
                }
                return;
            } else {
                if (![self.persistence storeMessageForClientId:self.clientId
                                                         topic:topic
                                                          data:data
                                                    retainFlag:msg.retainFlag
                                                           qos:msg.qos
                                                         msgId:msgId
                                                  incomingFlag:YES
                                                   commandType:ZYJMQTTPubrec
                                                      deadline:[NSDate dateWithTimeIntervalSinceNow:self.dupTimeout]]) {
                    NSLog(@"[ZYJMQTTSession] dropping incoming messages");
                } else {
                    [self.persistence sync];
                    [self tell];
                    (void)[self encode:[ZYJMQTTMessage pubrecMessageWithMessageId:msgId
                                                                 protocolLevel:self.protocolLevel
                                                                    returnCode:ZYJMQTTSuccess
                                                                  reasonString:nil
                                                                  userProperty:nil]];
                }
            }
        }
    }
}

- (void)handlePuback:(ZYJMQTTMessage*)msg {
    id<ZYJMQTTFlow> flow = [self.persistence flowforClientId:self.clientId
                                             incomingFlag:NO
                                                messageId:msg.mid];
    if (flow) {
        if ((flow.commandType).intValue == ZYJMQTTPublish && (flow.qosLevel).intValue == ZYJMQTTQosLevelAtLeastOnce) {
            if ([self.delegate respondsToSelector:@selector(messageDelivered:msgID:)]) {
                [self.delegate messageDelivered:self msgID:msg.mid];
            }
            if ([self.delegate respondsToSelector:@selector(messageDelivered:msgID:topic:data:qos:retainFlag:)]) {
                [self.delegate messageDelivered:self
                                          msgID:msg.mid
                                          topic:flow.topic
                                           data:flow.data
                                            qos:(flow.qosLevel).intValue
                                     retainFlag:(flow.retainedFlag).boolValue];
            }
            if (self.synchronPub && self.synchronPubMid == msg.mid) {
                self.synchronPub = FALSE;
            }
            ZYJMQTTPublishHandler publishHandler = (self.publishHandlers)[@(msg.mid)];
            if (publishHandler) {
                [self.publishHandlers removeObjectForKey:@(msg.mid)];
                [self onPublish:publishHandler error:nil];
            }
            [self.persistence deleteFlow:flow];
            [self.persistence sync];
            [self tell];
        }
    }
}

- (void)handleSuback:(ZYJMQTTMessage*)msg
{
    if (msg.data.length >= 3) {
        UInt8 const *bytes = msg.data.bytes;
        UInt16 messageId = (256 * bytes[0] + bytes[1]);
        msg.mid = messageId;
        NSMutableArray *qoss = [[NSMutableArray alloc] init];
        for (int i = 2; i < msg.data.length; i++) {
            [qoss addObject:@(bytes[i])];
        }
        if ([self.delegate respondsToSelector:@selector(subAckReceived:msgID:grantedQoss:)]) {
            [self.delegate subAckReceived:self msgID:msg.mid grantedQoss:qoss];
        }
        if (self.synchronSub && self.synchronSubMid == msg.mid) {
            self.synchronSub = FALSE;
        }
        ZYJMQTTSubscribeHandler subscribeHandler = (self.subscribeHandlers)[@(msg.mid)];
        if (subscribeHandler) {
            [self.subscribeHandlers removeObjectForKey:@(msg.mid)];
            [self onSubscribe:subscribeHandler error:nil gQoss:qoss];
        }
    }
}

- (void)handleUnsuback:(ZYJMQTTMessage *)message {
    if ([self.delegate respondsToSelector:@selector(unsubAckReceived:msgID:)]) {
        [self.delegate unsubAckReceived:self msgID:message.mid];
    }
    if (self.synchronUnsub && self.synchronUnsubMid == message.mid) {
        self.synchronUnsub = FALSE;
    }
    ZYJMQTTUnsubscribeHandler unsubscribeHandler = (self.unsubscribeHandlers)[@(message.mid)];
    if (unsubscribeHandler) {
        [self.unsubscribeHandlers removeObjectForKey:@(message.mid)];
        [self onUnsubscribe:unsubscribeHandler error:nil];
    }
}

- (void)handlePubrec:(ZYJMQTTMessage *)message {
    ZYJMQTTMessage *pubrelmessage = [ZYJMQTTMessage pubrelMessageWithMessageId:message.mid
                                                           protocolLevel:self.protocolLevel
                                                              returnCode:ZYJMQTTSuccess
                                                            reasonString:nil
                                                            userProperty:nil];
    id<ZYJMQTTFlow> flow = [self.persistence flowforClientId:self.clientId
                                             incomingFlag:NO
                                                messageId:message.mid];
    if (flow) {
        if ((flow.commandType).intValue == ZYJMQTTPublish && (flow.qosLevel).intValue == ZYJMQTTQosLevelExactlyOnce) {
            flow.commandType = @(ZYJMQTTPubrel);
            flow.deadline = [NSDate dateWithTimeIntervalSinceNow:self.dupTimeout];
            [self.persistence sync];
        }
    }
    (void)[self encode:pubrelmessage];
}

- (void)handlePubrel:(ZYJMQTTMessage *)message {
    id<ZYJMQTTFlow> flow = [self.persistence flowforClientId:self.clientId
                                             incomingFlag:YES
                                                messageId:message.mid];
    if (flow) {
        BOOL processed = true;
        NSData *data = flow.data;
        if (self.protocolLevel == ZYJMQTTProtocolVersion50) {
            int propertiesLength = [ZYJMQTTProperties getVariableLength:data];
            int variableLength = [ZYJMQTTProperties variableIntLength:propertiesLength];
            NSRange range = NSMakeRange(variableLength + propertiesLength, data.length - variableLength - propertiesLength);
            data = [data subdataWithRange:range];
        }


        if ([self.delegate respondsToSelector:@selector(newMessage:data:onTopic:qos:retained:mid:)]) {
            [self.delegate newMessage:self
                                 data:data
                              onTopic:flow.topic
                                  qos:(flow.qosLevel).intValue
                             retained:(flow.retainedFlag).boolValue
                                  mid:(flow.messageId).intValue
             ];
        }
        if ([self.delegate respondsToSelector:@selector(newMessageWithFeedback:data:onTopic:qos:retained:mid:)]) {
            processed = [self.delegate newMessageWithFeedback:self
                                                         data:data
                                                      onTopic:flow.topic
                                                          qos:(flow.qosLevel).intValue
                                                     retained:(flow.retainedFlag).boolValue
                                                          mid:(flow.messageId).intValue
                         ];
        }
        if(self.messageHandler){
            self.messageHandler(flow.data, flow.topic);
        }
        if (processed) {
            [self.persistence deleteFlow:flow];
            [self.persistence sync];
            [self tell];
            (void)[self encode:[ZYJMQTTMessage pubcompMessageWithMessageId:message.mid
                                                          protocolLevel:self.protocolLevel
                                                             returnCode:ZYJMQTTSuccess
                                                           reasonString:nil
                                                           userProperty:nil]];
        }
    }
}

- (void)handlePubcomp:(ZYJMQTTMessage *)message {
    id<ZYJMQTTFlow> flow = [self.persistence flowforClientId:self.clientId
                                             incomingFlag:NO
                                                messageId:message.mid];
    if (flow && (flow.commandType).intValue == ZYJMQTTPubrel) {
        if ([self.delegate respondsToSelector:@selector(messageDelivered:msgID:)]) {
            [self.delegate messageDelivered:self msgID:message.mid];
        }
        if ([self.delegate respondsToSelector:@selector(messageDelivered:msgID:topic:data:qos:retainFlag:)]) {
            [self.delegate messageDelivered:self
                                      msgID:message.mid
                                      topic:flow.topic
                                       data:flow.data
                                        qos:(flow.qosLevel).intValue
                                 retainFlag:(flow.retainedFlag).boolValue];
        }

        if (self.synchronPub && self.synchronPubMid == message.mid) {
            self.synchronPub = FALSE;
        }
        ZYJMQTTPublishHandler publishHandler = (self.publishHandlers)[@(message.mid)];
        if (publishHandler) {
            [self.publishHandlers removeObjectForKey:@(message.mid)];
            [self onPublish:publishHandler error:nil];
        }
        [self.persistence deleteFlow:flow];
        [self.persistence sync];
        [self tell];
    }
}

- (void)connectionError:(NSError *)error {
    [self error:ZYJMQTTSessionEventConnectionError error:error];
    if ([self.delegate respondsToSelector:@selector(connectionError:error:)]) {
        [self.delegate connectionError:self error:error];
    }
    if (self.connectHandler) {
        ZYJMQTTConnectHandler connectHandler = self.connectHandler;
        self.connectHandler = nil;
        [self onConnect:connectHandler error:error];
    }
}

- (void)protocolError:(NSError *)error {
    [self error:ZYJMQTTSessionEventProtocolError error:error];
    if ([self.delegate respondsToSelector:@selector(protocolError:error:)]) {
        [self.delegate protocolError:self error:error];
    }
}

- (void)error:(ZYJMQTTSessionEvent)eventCode error:(NSError *)error {
    self.status = ZYJMQTTSessionStatusError;
    if ([self.delegate respondsToSelector:@selector(handleEvent:event:error:)]) {
        [self.delegate handleEvent:self event:eventCode error:error];
    }
    [self closeInternal];
    
    if(self.connectionHandler){
        self.connectionHandler(eventCode);
    }

    if (eventCode == ZYJMQTTSessionEventConnectionClosedByBroker && self.connectHandler) {
        error = [NSError errorWithDomain:ZYJMQTTSessionErrorDomain
                                    code:ZYJMQTTSessionErrorConnectionRefused
                                userInfo:@{NSLocalizedDescriptionKey : @"Server has closed connection without connack."}];

        ZYJMQTTConnectHandler connectHandler = self.connectHandler;
        self.connectHandler = nil;
        [self onConnect:connectHandler error:error];
    }

    self.synchronPub = FALSE;
    self.synchronPubMid = 0;
    self.synchronSub = FALSE;
    self.synchronSubMid = 0;
    self.synchronUnsub = FALSE;
    self.synchronUnsubMid = 0;
    self.synchronConnect = FALSE;
    self.synchronDisconnect = FALSE;
}

- (UInt16)nextMsgId {
//    NSLog(@"nextMsgId synchronizing");
    @synchronized(self) {
//        NSLog(@"nextMsgId synchronized");
        self.txMsgId++;
        while (self.txMsgId == 0 || [self.persistence flowforClientId:self.clientId
                                                         incomingFlag:NO
                                                            messageId:self.txMsgId] != nil) {
            self.txMsgId++;
        }
//        NSLog(@"nextMsgId synchronized done");
        return self.txMsgId;
    }
}

- (void)tell {
    NSUInteger incoming = [self.persistence allFlowsforClientId:self.clientId
                                                   incomingFlag:YES].count;
    NSUInteger outflowing = [self.persistence allFlowsforClientId:self.clientId
                                                     incomingFlag:NO].count;
    if ([self.delegate respondsToSelector:@selector(buffered:flowingIn:flowingOut:)]) {
        [self.delegate buffered:self
                      flowingIn:incoming
                     flowingOut:outflowing];
    }
    if ([self.delegate respondsToSelector:@selector(buffered:queued:flowingIn:flowingOut:)]) {
        [self.delegate buffered:self
                         queued:0
                      flowingIn:incoming
                     flowingOut:outflowing];
    }
}

- (void)onConnect:(ZYJMQTTConnectHandler)connectHandler error:(NSError *)error {
    connectHandler(error);
}

- (void)onDisconnect:(ZYJMQTTDisconnectHandler)disconnectHandler error:(NSError *)error {
    disconnectHandler(error);
}

- (void)onSubscribe:(ZYJMQTTSubscribeHandler)subscribeHandler error:(NSError *)error gQoss:(NSArray *)gqoss {
    subscribeHandler(error, gqoss);
}

- (void)onUnsubscribe:(ZYJMQTTUnsubscribeHandler)unsubscribeHandler error:(NSError *)error {
    unsubscribeHandler(error);
}

- (void)onPublish:(ZYJMQTTPublishHandler)publishHandler error:(NSError *)error {
    publishHandler(error);
}

#pragma mark - ZYJMQTTTransport interface

- (void)connect {

    if (ZYJMQTTStrict.strict &&
        self.clientId && self.clientId.length < 1 &&
        !self.cleanSessionFlag) {
        NSException* myException = [NSException
                                    exceptionWithName:@"clientId must be at least 1 character long if cleanSessionFlag is off"
                                    reason:[NSString stringWithFormat:@"clientId length = %lu",
                                            (unsigned long)[self.clientId dataUsingEncoding:NSUTF8StringEncoding].length]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        !self.clientId) {
        NSException* myException = [NSException
                                    exceptionWithName:@"clientId must not be nil"
                                    reason:[NSString stringWithFormat:@"clientId length = %lu",
                                            (unsigned long)[self.clientId dataUsingEncoding:NSUTF8StringEncoding].length]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        [self.clientId dataUsingEncoding:NSUTF8StringEncoding].length > 65535L) {
        NSException* myException = [NSException
                                    exceptionWithName:@"clientId may not be longer than 65535 bytes in UTF8 representation"
                                    reason:[NSString stringWithFormat:@"clientId length = %lu",
                                            (unsigned long)[self.clientId dataUsingEncoding:NSUTF8StringEncoding].length]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        ![self.clientId dataUsingEncoding:NSUTF8StringEncoding]) {
        NSException* myException = [NSException
                                    exceptionWithName:@"clientId must not contain non-UTF8 characters"
                                    reason:[NSString stringWithFormat:@"clientId = %@", self.clientId]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        [self.userName dataUsingEncoding:NSUTF8StringEncoding].length > 65535L) {
        NSException* myException = [NSException
                                    exceptionWithName:@"userName may not be longer than 65535 bytes in UTF8 representation"
                                    reason:[NSString stringWithFormat:@"userName length = %lu",
                                            (unsigned long)[self.userName dataUsingEncoding:NSUTF8StringEncoding].length]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        ![self.userName dataUsingEncoding:NSUTF8StringEncoding]) {
        NSException* myException = [NSException
                                    exceptionWithName:@"userName must not contain non-UTF8 characters"
                                    reason:[NSString stringWithFormat:@"userName = %@", self.userName]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        !self.userName) {
        NSException* myException = [NSException
                                    exceptionWithName:@"password specified without userName"
                                    reason:[NSString stringWithFormat:@"password = %@", self.password]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.protocolLevel != ZYJMQTTProtocolVersion31 &&
        self.protocolLevel != ZYJMQTTProtocolVersion311 &&
        self.protocolLevel != ZYJMQTTProtocolVersion50) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Illegal protocolLevel"
                                    reason:[NSString stringWithFormat:@"%d is not 3, 4, or 5", self.protocolLevel]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        !self.willFlag &&
        self.willTopic) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Will topic must be nil if willFlag is false"
                                    reason:[NSString stringWithFormat:@"%@", self.willTopic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        !self.willFlag &&
        self.willMsg) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Will message must be nil if willFlag is false"
                                    reason:[NSString stringWithFormat:@"%@", self.willMsg]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        !self.willFlag &&
        self.willRetainFlag) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Will retain must be false if willFlag is false"
                                    reason:[NSString stringWithFormat:@"%d", self.willRetainFlag]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        !self.willFlag &&
        self.willQoS != ZYJMQTTQosLevelAtMostOnce) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Will QoS Level must be 0 if willFlag is false"
                                    reason:[NSString stringWithFormat:@"%d", self.willQoS]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.willQoS != ZYJMQTTQosLevelAtMostOnce &&
        self.willQoS != ZYJMQTTQosLevelAtLeastOnce &&
        self.willQoS != ZYJMQTTQosLevelExactlyOnce) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Illegal will QoS level"
                                    reason:[NSString stringWithFormat:@"%d is not 0, 1, or 2", self.willQoS]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.willFlag &&
        !self.willTopic) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Will topic must not be nil if willFlag is true"
                                    reason:[NSString stringWithFormat:@"%@", self.willTopic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.willTopic &&
        self.willTopic.length < 1) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Will topic must be at least 1 character long"
                                    reason:[NSString stringWithFormat:@"%@", self.willTopic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.willTopic &&
        [self.willTopic dataUsingEncoding:NSUTF8StringEncoding].length > 65535L) {
        NSException* myException = [NSException
                                    exceptionWithName:@"willTopic may not be longer than 65535 bytes in UTF8 representation"
                                    reason:[NSString stringWithFormat:@"willTopic length = %lu",
                                            (unsigned long)[self.willTopic dataUsingEncoding:NSUTF8StringEncoding].length]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.willTopic &&
        ![self.willTopic dataUsingEncoding:NSUTF8StringEncoding]) {
        NSException* myException = [NSException
                                    exceptionWithName:@"willTopic must not contain non-UTF8 characters"
                                    reason:[NSString stringWithFormat:@"willTopic = %@", self.willTopic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.willTopic &&
        ([self.willTopic containsString:@"+"] ||
         [self.willTopic containsString:@"#"])
        ) {
        NSException* myException = [NSException
                                    exceptionWithName:@"willTopic must not contain wildcards"
                                    reason:[NSString stringWithFormat:@"willTopic = %@", self.self.willTopic]
                                    userInfo:nil];
        @throw myException;
    }

    if (ZYJMQTTStrict.strict &&
        self.willFlag &&
        !self.willMsg) {
        NSException* myException = [NSException
                                    exceptionWithName:@"Will message must not be nil if willFlag is true"
                                    reason:[NSString stringWithFormat:@"%@", self.willMsg]
                                    userInfo:nil];
        @throw myException;
    }

    NSLog(@"[ZYJMQTTSession] connecting");
    if (self.cleanSessionFlag) {
        [self.persistence deleteAllFlowsForClientId:self.clientId];
        [self.subscribeHandlers removeAllObjects];
        [self.unsubscribeHandlers removeAllObjects];
        [self.publishHandlers removeAllObjects];
    }
    [self tell];

    self.status = ZYJMQTTSessionStatusConnecting;

    self.decoder = [[ZYJMQTTDecoder alloc] init];
    self.decoder.queue = self.queue;
    self.decoder.delegate = self;
    [self.decoder open];

    self.transport.delegate = self;
    [self.transport open];
}

- (void)connectWithConnectHandler:(ZYJMQTTConnectHandler)connectHandler {
    NSLog(@"[ZYJMQTTSession] connectWithConnectHandler:%p", connectHandler);
    self.connectHandler = connectHandler;
    [self connect];
}

- (BOOL)encode:(ZYJMQTTMessage *)message {
    if (message) {
        NSData *wireFormat = message.wireFormat;
        if (wireFormat) {
            if (self.delegate) {
                if ([self.delegate respondsToSelector:@selector(sending:type:qos:retained:duped:mid:data:)]) {
                    [self.delegate sending:self
                                      type:message.type
                                       qos:message.qos
                                  retained:message.retainFlag
                                     duped:message.dupFlag
                                       mid:message.mid
                                      data:message.data];
                }
            }
//            NSLog(@"[ZYJMQTTSession] ZYJMQTTTransport send");
            return [self.transport send:wireFormat];
        } else {
//            NSLog(@"[ZYJMQTTSession] trying to send message without wire format");
            return false;
        }
    } else {
//        NSLog(@"[ZYJMQTTSession] trying to send nil message");
        return false;
    }
}

#pragma mark - ZYJMQTTTransport delegate
- (void)ZYJMQTTTransport:(id<ZYJMQTTTransport>)ZYJMQTTTransport didReceiveMessage:(NSData *)message {
//    NSLog(@"[ZYJMQTTSession] ZYJMQTTTransport didReceiveMessage");

    [self.decoder decodeMessage:message];

}

- (void)ZYJMQTTTransportDidClose:(id<ZYJMQTTTransport>)ZYJMQTTTransport {
    NSLog(@"[ZYJMQTTSession] ZYJMQTTTransport ZYJMQTTTransportDidClose");

    [self error:ZYJMQTTSessionEventConnectionClosedByBroker error:nil];

}

- (void)ZYJMQTTTransportDidOpen:(id<ZYJMQTTTransport>)ZYJMQTTTransport {
    NSLog(@"[ZYJMQTTSession] ZYJMQTTTransportDidOpen");

    NSLog(@"[ZYJMQTTSession] sending CONNECT");

    if (!self.connectMessage) {
        (void)[self encode:[ZYJMQTTMessage connectMessageWithClientId:self.clientId
                                                          userName:self.userName
                                                          password:self.password
                                                         keepAlive:self.keepAliveInterval
                                                      cleanSession:self.cleanSessionFlag
                                                              will:self.willFlag
                                                         willTopic:self.willTopic
                                                           willMsg:self.willMsg
                                                           willQoS:self.willQoS
                                                        willRetain:self.willRetainFlag
                                                     protocolLevel:self.protocolLevel
                                             sessionExpiryInterval:self.sessionExpiryInterval
                                                        authMethod:self.authMethod
                                                          authData:self.authData
                                         requestProblemInformation:self.requestProblemInformation
                                                 willDelayInterval:self.willDelayInterval
                                        requestResponseInformation:self.requestResponseInformation
                                                    receiveMaximum:self.receiveMaximum
                                                 topicAliasMaximum:self.topicAliasMaximum
                                                      userProperty:self.userProperty
                                                 maximumPacketSize:self.maximumPacketSize]];
    } else {
        (void)[self encode:self.connectMessage];
    }
}

- (void)ZYJMQTTTransport:(id<ZYJMQTTTransport>)ZYJMQTTTransport didFailWithError:(NSError *)error {
    NSLog(@"[ZYJMQTTSession] ZYJMQTTTransport didFailWithError %@", error);
    
    [self connectionError:error];
}
@end
