//
//  ZYJMQTTSessionManager.m
//  ZYJMQTTClient
//
//  Created by Christoph Krey on 09.07.14.
//  Copyright © 2013-2017 Christoph Krey. All rights reserved.
//

#import "ZYJMQTTSessionManager.h"
#import "ZYJMQTTCoreDataPersistence.h"
#import "ZYJMQTTLog.h"
#import "ZYJReconnectTimer.h"
#import "ZYJForegroundReconnection.h"
#import "ZYJMQTTSSLSecurityPolicyTransport.h"

@interface ZYJMQTTSessionManager()

@property (nonatomic, readwrite) ZYJMQTTSessionManagerState state;
@property (nonatomic, readwrite) NSError *lastErrorCode;

@property (strong, nonatomic) ZYJReconnectTimer *reconnectTimer;
@property (nonatomic) BOOL reconnectFlag;

@property (strong, nonatomic) ZYJMQTTSession *session;

@property (strong, nonatomic) NSString *host;
@property (nonatomic) UInt32 port;
@property (nonatomic) BOOL tls;
@property (nonatomic) NSInteger keepalive;
@property (nonatomic) BOOL clean;
@property (nonatomic) BOOL auth;
@property (nonatomic) BOOL will;
@property (strong, nonatomic) NSString *user;
@property (strong, nonatomic) NSString *pass;
@property (strong, nonatomic) NSString *willTopic;
@property (strong, nonatomic) NSData *willMsg;
@property (nonatomic) NSInteger willQos;
@property (nonatomic) BOOL willRetainFlag;
@property (strong, nonatomic) NSString *clientId;
@property (strong, nonatomic) dispatch_queue_t queue;
@property (strong, nonatomic) ZYJMQTTSSLSecurityPolicy *securityPolicy;
@property (strong, nonatomic) NSArray *certificates;
@property (nonatomic) ZYJMQTTProtocolVersion protocolLevel;

#if TARGET_OS_IPHONE == 1
@property (strong, nonatomic) ZYJForegroundReconnection *foregroundReconnection;
#endif

@property (nonatomic) BOOL persistent;
@property (nonatomic) NSUInteger maxWindowSize;
@property (nonatomic) NSUInteger maxSize;
@property (nonatomic) NSUInteger maxMessages;
@property (strong, nonatomic) NSString *streamSSLLevel;

@property (strong, nonatomic) NSDictionary<NSString *, NSNumber *> *internalSubscriptions;
@property (strong, nonatomic) NSDictionary<NSString *, NSNumber *> *effectiveSubscriptions;
@property (strong, nonatomic) NSLock *subscriptionLock;

@end

#define RECONNECT_TIMER 1.0
#define RECONNECT_TIMER_MAX_DEFAULT 24.0

@implementation ZYJMQTTSessionManager

- (instancetype)init {
    self = [self initWithPersistence:ZYJMQTT_PERSISTENT
                       maxWindowSize:ZYJMQTT_MAX_WINDOW_SIZE
                         maxMessages:ZYJMQTT_MAX_MESSAGES
                             maxSize:ZYJMQTT_MAX_SIZE
          maxConnectionRetryInterval:RECONNECT_TIMER_MAX_DEFAULT
                 connectInForeground:YES
                      streamSSLLevel:(NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL
                               queue:dispatch_get_main_queue()];
    return self;
}

- (ZYJMQTTSessionManager *)initWithPersistence:(BOOL)persistent
                              maxWindowSize:(NSUInteger)maxWindowSize
                                maxMessages:(NSUInteger)maxMessages
                                    maxSize:(NSUInteger)maxSize
                 maxConnectionRetryInterval:(NSTimeInterval)maxRetryInterval
                        connectInForeground:(BOOL)connectInForeground
                             streamSSLLevel:(NSString *)streamSSLLevel
                                      queue:(dispatch_queue_t)queue {
    self = [super init];
    self.streamSSLLevel = streamSSLLevel;
    self.queue = queue;
    [self updateState:ZYJMQTTSessionManagerStateStarting];
    self.internalSubscriptions = [[NSMutableDictionary alloc] init];
    self.effectiveSubscriptions = [[NSMutableDictionary alloc] init];
    self.persistent = persistent;
    self.maxWindowSize = maxWindowSize;
    self.maxSize = maxSize;
    self.maxMessages = maxMessages;
    
    __weak ZYJMQTTSessionManager *weakSelf = self;
    self.reconnectTimer = [[ZYJReconnectTimer alloc] initWithRetryInterval:RECONNECT_TIMER
                                                       maxRetryInterval:maxRetryInterval
                                                                  queue:self.queue
                                                         reconnectBlock:^{
                                                             [weakSelf reconnect:nil];
                                                         }];
#if TARGET_OS_IPHONE == 1
    if (connectInForeground) {
        self.foregroundReconnection = [[ZYJForegroundReconnection alloc] initWithZYJMQTTSessionManager:self];
    }
#endif
    self.subscriptionLock = [[NSLock alloc] init];
    
    return self;
}

- (void)connectTo:(NSString *)host
             port:(NSInteger)port
              tls:(BOOL)tls
        keepalive:(NSInteger)keepalive
            clean:(BOOL)clean
             auth:(BOOL)auth
             user:(NSString *)user
             pass:(NSString *)pass
             will:(BOOL)will
        willTopic:(NSString *)willTopic
          willMsg:(NSData *)willMsg
          willQos:(ZYJMQTTQosLevel)willQos
   willRetainFlag:(BOOL)willRetainFlag
     withClientId:(NSString *)clientId
   securityPolicy:(ZYJMQTTSSLSecurityPolicy *)securityPolicy
     certificates:(NSArray *)certificates
    protocolLevel:(ZYJMQTTProtocolVersion)protocolLevel
   connectHandler:(ZYJMQTTConnectHandler)connectHandler {
//    NSLog(@"ZYJMQTTSessionManager connectTo:%@", host);
    BOOL shouldReconnect = self.session != nil;
    if (!self.session ||
        ![host isEqualToString:self.host] ||
        port != self.port ||
        tls != self.tls ||
        keepalive != self.keepalive ||
        clean != self.clean ||
        auth != self.auth ||
        ![user isEqualToString:self.user] ||
        ![pass isEqualToString:self.pass] ||
        ![willTopic isEqualToString:self.willTopic] ||
        ![willMsg isEqualToData:self.willMsg] ||
        willQos != self.willQos ||
        willRetainFlag != self.willRetainFlag ||
        ![clientId isEqualToString:self.clientId] ||
        securityPolicy != self.securityPolicy ||
        certificates != self.certificates) {
        self.host = host;
        self.port = (int)port;
        self.tls = tls;
        self.keepalive = keepalive;
        self.clean = clean;
        self.auth = auth;
        self.user = user;
        self.pass = pass;
        self.will = will;
        self.willTopic = willTopic;
        self.willMsg = willMsg;
        self.willQos = willQos;
        self.willRetainFlag = willRetainFlag;
        self.clientId = clientId;
        self.securityPolicy = securityPolicy;
        self.certificates = certificates;
        self.protocolLevel = protocolLevel;
        
        self.session = [[ZYJMQTTSession alloc] initWithClientId:clientId
                                                    userName:auth ? user : nil
                                                    password:auth ? pass : nil
                                                   keepAlive:keepalive
                                              connectMessage:nil
                                                cleanSession:clean
                                                        will:will
                                                   willTopic:willTopic
                                                     willMsg:willMsg
                                                     willQoS:willQos
                                              willRetainFlag:willRetainFlag
                                               protocolLevel:protocolLevel
                                                       queue:self.queue
                                              securityPolicy:securityPolicy
                                                certificates:certificates];
        self.session.streamSSLLevel = self.streamSSLLevel;
        ZYJMQTTCoreDataPersistence *persistence = [[ZYJMQTTCoreDataPersistence alloc] init];
        
        persistence.persistent = self.persistent;
        persistence.maxWindowSize = self.maxWindowSize;
        persistence.maxSize = self.maxSize;
        persistence.maxMessages = self.maxMessages;
        
        self.session.persistence = persistence;
        
        self.session.delegate = self;
        self.reconnectFlag = FALSE;
    }
    if (shouldReconnect) {
//        NSLog(@"[ZYJMQTTSessionManager] reconnecting");
//        [self disconnectWithDisconnectHandler:nil];
        
        [self updateState:ZYJMQTTSessionManagerStateClosing];
        [self.session closeWithDisconnectHandler:nil];
        [self.reconnectTimer stop];
        
        [self reconnect:connectHandler];
    } else {
//        NSLog(@"[ZYJMQTTSessionManager] connecting");
//        [self connectToInternal:connectHandler];
        
        [self reconnect:connectHandler];
    }
}

- (UInt16)sendData:(NSData *)data topic:(NSString *)topic qos:(ZYJMQTTQosLevel)qos retain:(BOOL)retainFlag {
    if (self.state != ZYJMQTTSessionManagerStateConnected) {
        [self connectToLast:nil];
    }
    UInt16 msgId = [self.session publishData:data
                                     onTopic:topic
                                      retain:retainFlag
                                         qos:qos];
    return msgId;
}

- (void)disconnectWithDisconnectHandler:(ZYJMQTTDisconnectHandler)disconnectHandler {
    [self updateState:ZYJMQTTSessionManagerStateClosing];
    [self.session closeWithDisconnectHandler:disconnectHandler];
    [self.reconnectTimer stop];
    self.session = nil;
}

- (BOOL)requiresTearDown {
    return (self.state != ZYJMQTTSessionManagerStateClosed &&
            self.state != ZYJMQTTSessionManagerStateStarting);
}

- (void)updateState:(ZYJMQTTSessionManagerState)newState {
    self.state = newState;
    
    if ([self.delegate respondsToSelector:@selector(sessionManager:didChangeState:)]) {
        [self.delegate sessionManager:self didChangeState:newState];
    }
}


#pragma mark - ZYJMQTT Callback methods

- (void)handleEvent:(ZYJMQTTSession *)session event:(ZYJMQTTSessionEvent)eventCode error:(NSError *)error {
#ifdef DEBUG
    __unused const NSDictionary *events = @{
                                            @(ZYJMQTTSessionEventConnected): @"connected",
                                            @(ZYJMQTTSessionEventConnectionRefused): @"connection refused",
                                            @(ZYJMQTTSessionEventConnectionClosed): @"connection closed",
                                            @(ZYJMQTTSessionEventConnectionError): @"connection error",
                                            @(ZYJMQTTSessionEventProtocolError): @"protocoll error",
                                            @(ZYJMQTTSessionEventConnectionClosedByBroker): @"connection closed by broker"
                                            };
    NSLog(@"[ZYJMQTTSessionManager] eventCode: %@ (%ld) %@", events[@(eventCode)], (long)eventCode, error);
#endif
    switch (eventCode) {
        case ZYJMQTTSessionEventConnected:
            self.lastErrorCode = nil;
            [self updateState:ZYJMQTTSessionManagerStateConnected];
            [self.reconnectTimer resetRetryInterval];
            break;
            
        case ZYJMQTTSessionEventConnectionClosed:
            [self updateState:ZYJMQTTSessionManagerStateClosed];
            break;
            
        case ZYJMQTTSessionEventConnectionClosedByBroker:
            if (self.state != ZYJMQTTSessionManagerStateClosing) {
                [self triggerDelayedReconnect];
            }
            [self updateState:ZYJMQTTSessionManagerStateClosed];
            break;
            
        case ZYJMQTTSessionEventProtocolError:
        case ZYJMQTTSessionEventConnectionRefused:
        case ZYJMQTTSessionEventConnectionError:
            [self triggerDelayedReconnect];
            self.lastErrorCode = error;
            [self updateState:ZYJMQTTSessionManagerStateError];
            break;
            
        default:
            break;
    }
}

- (void)newMessage:(ZYJMQTTSession *)session data:(NSData *)data onTopic:(NSString *)topic qos:(ZYJMQTTQosLevel)qos retained:(BOOL)retained mid:(unsigned int)mid {
    if (self.delegate) {
        if ([self.delegate respondsToSelector:@selector(sessionManager:didReceiveMessage:onTopic:retained:)]) {
            [self.delegate sessionManager:self didReceiveMessage:data onTopic:topic retained:retained];
        }
        if ([self.delegate respondsToSelector:@selector(handleMessage:onTopic:retained:)]) {
            [self.delegate handleMessage:data onTopic:topic retained:retained];
        }
    }
}

- (void)connected:(ZYJMQTTSession *)session sessionPresent:(BOOL)sessionPresent {
    if (self.clean || !self.reconnectFlag || !sessionPresent) {
        NSDictionary *subscriptions = [self.internalSubscriptions copy];
        [self.subscriptionLock lock];
        self.effectiveSubscriptions = [[NSMutableDictionary alloc] init];
        [self.subscriptionLock unlock];
        if (subscriptions.count) {
            __weak ZYJMQTTSessionManager *weakSelf = self;
            [self.session subscribeToTopics:subscriptions subscribeHandler:^(NSError *error, NSArray<NSNumber *> *gQoss) {
                ZYJMQTTSessionManager *strongSelf = weakSelf;
                if (!error) {
                    NSArray<NSString *> *allTopics = subscriptions.allKeys;
                    for (int i = 0; i < allTopics.count; i++) {
                        NSString *topic = allTopics[i];
                        NSNumber *gQos = gQoss[i];
                        [strongSelf.subscriptionLock lock];
                        NSMutableDictionary *newEffectiveSubscriptions = [strongSelf.subscriptions mutableCopy];
                        newEffectiveSubscriptions[topic] = gQos;
                        strongSelf.effectiveSubscriptions = newEffectiveSubscriptions;
                        [strongSelf.subscriptionLock unlock];
                    }
                }
            }];
            
        }
        self.reconnectFlag = TRUE;
    }
}

- (void)messageDelivered:(ZYJMQTTSession *)session msgID:(UInt16)msgID {
    if (self.delegate) {
        if ([self.delegate respondsToSelector:@selector(sessionManager:didDeliverMessage:)]) {
            [self.delegate sessionManager:self didDeliverMessage:msgID];
        }
        if ([self.delegate respondsToSelector:@selector(messageDelivered:)]) {
            [self.delegate messageDelivered:msgID];
        }
    }
}


- (void)connectToInternal:(ZYJMQTTConnectHandler)connectHandler {
    if (self.session && self.state == ZYJMQTTSessionManagerStateStarting) {
        [self updateState:ZYJMQTTSessionManagerStateConnecting];
        ZYJMQTTCFSocketTransport *transport;
        if (self.securityPolicy) {
            transport = [[ZYJMQTTSSLSecurityPolicyTransport alloc] init];
            ((ZYJMQTTSSLSecurityPolicyTransport *)transport).securityPolicy = self.securityPolicy;
        } else {
            transport = [[ZYJMQTTCFSocketTransport alloc] init];
        }
        transport.host = self.host;
        transport.port = self.port;
        transport.tls = self.tls;
        transport.certificates = self.certificates;
        transport.voip = self.session.voip;
        transport.queue = self.queue;
        transport.streamSSLLevel = self.streamSSLLevel;
        self.session.transport = transport;
        [self.session connectWithConnectHandler:connectHandler];
    }
}

- (void)reconnect:(ZYJMQTTConnectHandler)connectHandler {
    [self updateState:ZYJMQTTSessionManagerStateStarting];
    [self connectToInternal:connectHandler];
}

- (void)connectToLast:(ZYJMQTTConnectHandler)connectHandler {
    if (self.state == ZYJMQTTSessionManagerStateConnected) {
        return;
    }
    [self.reconnectTimer resetRetryInterval];
    [self reconnect:connectHandler];
}

- (void)triggerDelayedReconnect {
    [self.reconnectTimer schedule];
}

- (NSDictionary<NSString *, NSNumber *> *)subscriptions {
    return self.internalSubscriptions;
}

- (void)setSubscriptions:(NSDictionary<NSString *, NSNumber *> *)newSubscriptions {
    if (self.state == ZYJMQTTSessionManagerStateConnected) {
        NSDictionary *currentSubscriptions = [self.effectiveSubscriptions copy];
        
        for (NSString *topicFilter in currentSubscriptions) {
            if (!newSubscriptions[topicFilter]) {
                __weak ZYJMQTTSessionManager *weakSelf = self;
                [self.session unsubscribeTopic:topicFilter unsubscribeHandler:^(NSError *error) {
                    ZYJMQTTSessionManager *strongSelf = weakSelf;
                    if (!error) {
                        [strongSelf.subscriptionLock lock];
                        NSMutableDictionary *newEffectiveSubscriptions = [strongSelf.subscriptions mutableCopy];
                        [newEffectiveSubscriptions removeObjectForKey:topicFilter];
                        strongSelf.effectiveSubscriptions = newEffectiveSubscriptions;
                        [strongSelf.subscriptionLock unlock];
                    }
                }];
            }
        }
        
        for (NSString *topicFilter in newSubscriptions) {
            if (!currentSubscriptions[topicFilter]) {
                NSNumber *number = newSubscriptions[topicFilter];
                ZYJMQTTQosLevel qos = number.unsignedIntValue;
                __weak ZYJMQTTSessionManager *weakSelf = self;
                [self.session subscribeToTopic:topicFilter atLevel:qos subscribeHandler:^(NSError *error, NSArray<NSNumber *> *gQoss) {
                    ZYJMQTTSessionManager *strongSelf = weakSelf;
                    if (!error) {
                        NSNumber *gQos = gQoss[0];
                        [strongSelf.subscriptionLock lock];
                        NSMutableDictionary *newEffectiveSubscriptions = [strongSelf.subscriptions mutableCopy];
                        newEffectiveSubscriptions[topicFilter] = gQos;
                        strongSelf.effectiveSubscriptions = newEffectiveSubscriptions;
                        [strongSelf.subscriptionLock unlock];
                    }
                }];
            }
        }
    }
    self.internalSubscriptions = newSubscriptions;
    NSLog(@"ZYJMQTTSessionManager internalSubscriptions: %@", self.internalSubscriptions);
}

@end
