//
//  ZYJMQTTCFSocketTransport.m
//  ZYJMQTTClient
//
//  Created by Christoph Krey on 06.12.15.
//  Copyright © 2015-2017 Christoph Krey. All rights reserved.
//

#import "ZYJMQTTCFSocketTransport.h"

#import "ZYJMQTTLog.h"

@interface ZYJMQTTCFSocketTransport() {
    void *QueueIdentityKey;
}

@property (strong, nonatomic) ZYJMQTTCFSocketEncoder *encoder;
@property (strong, nonatomic) ZYJMQTTCFSocketDecoder *decoder;

@end

@implementation ZYJMQTTCFSocketTransport

@synthesize state;
@synthesize delegate;
@synthesize queue = _queue;
@synthesize streamSSLLevel;
@synthesize host;
@synthesize port;

- (instancetype)init {
    self = [super init];
    self.host = @"localhost";
    self.port = 1883;
    self.tls = false;
    self.voip = false;
    self.certificates = nil;
    self.queue = dispatch_get_main_queue();
    self.streamSSLLevel = (NSString *)kCFStreamSocketSecurityLevelNegotiatedSSL;
    return self;
}

- (void)dealloc {
    [self close];
}

- (void)setQueue:(dispatch_queue_t)queue {
    _queue = queue;
    
    // We're going to use dispatch_queue_set_specific() to "mark" our queue.
    // The dispatch_queue_set_specific() and dispatch_get_specific() functions take a "void *key" parameter.
    // Later we can use dispatch_get_specific() to determine if we're executing on our queue.
    // From the documentation:
    //
    // > Keys are only compared as pointers and are never dereferenced.
    // > Thus, you can use a pointer to a static variable for a specific subsystem or
    // > any other value that allows you to identify the value uniquely.
    //
    // So we're just going to use the memory address of an ivar.
    
    dispatch_queue_set_specific(_queue, &QueueIdentityKey, (__bridge void *)_queue, NULL);
}

- (void)open {
    NSLog(@"[ZYJMQTTCFSocketTransport] open");
    self.state = ZYJMQTTTransportOpening;

    NSError* connectError;

    CFReadStreamRef readStream;
    CFWriteStreamRef writeStream;

    CFStreamCreatePairWithSocketToHost(NULL, (__bridge CFStringRef)self.host, self.port, &readStream, &writeStream);

    CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
    
    if (self.tls) {
        NSMutableDictionary *sslOptions = [[NSMutableDictionary alloc] init];
        
        sslOptions[(NSString *)kCFStreamSSLLevel] = self.streamSSLLevel;
        
        if (self.certificates) {
            sslOptions[(NSString *)kCFStreamSSLCertificates] = self.certificates;
        }
        
        if (!CFReadStreamSetProperty(readStream, kCFStreamPropertySSLSettings, (__bridge CFDictionaryRef)(sslOptions))) {
            connectError = [NSError errorWithDomain:@"ZYJMQTT"
                                               code:errSSLInternal
                                           userInfo:@{NSLocalizedDescriptionKey : @"Fail to init ssl input stream!"}];
        }
        if (!CFWriteStreamSetProperty(writeStream, kCFStreamPropertySSLSettings, (__bridge CFDictionaryRef)(sslOptions))) {
            connectError = [NSError errorWithDomain:@"ZYJMQTT"
                                               code:errSSLInternal
                                           userInfo:@{NSLocalizedDescriptionKey : @"Fail to init ssl output stream!"}];
        }
    }
    
    if (!connectError) {
        self.encoder.delegate = nil;
        self.encoder = [[ZYJMQTTCFSocketEncoder alloc] init];
        CFWriteStreamSetDispatchQueue(writeStream, self.queue);
        self.encoder.stream = CFBridgingRelease(writeStream);
        self.encoder.delegate = self;
        if (self.voip) {
            [self.encoder.stream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
        }
        [self.encoder open];
        
        self.decoder.delegate = nil;
        self.decoder = [[ZYJMQTTCFSocketDecoder alloc] init];
        CFReadStreamSetDispatchQueue(readStream, self.queue);
        self.decoder.stream =  CFBridgingRelease(readStream);
        self.decoder.delegate = self;
        if (self.voip) {
            [self.decoder.stream setProperty:NSStreamNetworkServiceTypeVoIP forKey:NSStreamNetworkServiceType];
        }
        [self.decoder open];
    } else {
        [self close];
    }
}

- (void)close {
    // https://github.com/novastone-media/ZYJMQTT-Client-Framework/issues/325
    // We need to make sure that we are closing streams on their queue
    // Otherwise, we end up with race condition where delegate is deallocated
    // but still used by run loop event
    if (self.queue != dispatch_get_specific(&QueueIdentityKey)) {
        dispatch_sync(self.queue, ^{
            [self internalClose];
        });
    } else {
        [self internalClose];
    }
}

- (void)internalClose {
    NSLog(@"[ZYJMQTTCFSocketTransport] close");
    self.state = ZYJMQTTTransportClosing;

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

- (BOOL)send:(nonnull NSData *)data {
    return [self.encoder send:data];
}

- (void)decoder:(ZYJMQTTCFSocketDecoder *)sender didReceiveMessage:(nonnull NSData *)data {
    [self.delegate ZYJMQTTTransport:self didReceiveMessage:data];
}

- (void)decoder:(ZYJMQTTCFSocketDecoder *)sender didFailWithError:(NSError *)error {
    //self.state = ZYJMQTTTransportClosing;
    //[self.delegate ZYJMQTTTransport:self didFailWithError:error];
}
- (void)encoder:(ZYJMQTTCFSocketEncoder *)sender didFailWithError:(NSError *)error {
    self.state = ZYJMQTTTransportClosing;
    [self.delegate ZYJMQTTTransport:self didFailWithError:error];
}

- (void)decoderdidClose:(ZYJMQTTCFSocketDecoder *)sender {
    self.state = ZYJMQTTTransportClosed;
    [self.delegate ZYJMQTTTransportDidClose:self];
}
- (void)encoderdidClose:(ZYJMQTTCFSocketEncoder *)sender {
    //self.state = ZYJMQTTTransportClosed;
    //[self.delegate ZYJMQTTTransportDidClose:self];
}

- (void)decoderDidOpen:(ZYJMQTTCFSocketDecoder *)sender {
    //self.state = ZYJMQTTTransportOpen;
    //[self.delegate ZYJMQTTTransportDidOpen:self];
}
- (void)encoderDidOpen:(ZYJMQTTCFSocketEncoder *)sender {
    self.state = ZYJMQTTTransportOpen;
    [self.delegate ZYJMQTTTransportDidOpen:self];
}

+ (NSArray *)clientCertsFromP12:(NSString *)path passphrase:(NSString *)passphrase {
    if (!path) {
        NSLog(@"[ZYJMQTTCFSocketTransport] no p12 path given");
        return nil;
    }
    
    NSData *pkcs12data = [[NSData alloc] initWithContentsOfFile:path];
    if (!pkcs12data) {
        NSLog(@"[ZYJMQTTCFSocketTransport] reading p12 failed");
        return nil;
    }
    
    if (!passphrase) {
        NSLog(@"[ZYJMQTTCFSocketTransport] no passphrase given");
        return nil;
    }
    CFArrayRef keyref = NULL;
    OSStatus importStatus = SecPKCS12Import((__bridge CFDataRef)pkcs12data,
                                            (__bridge CFDictionaryRef)@{(__bridge id)kSecImportExportPassphrase: passphrase},
                                            &keyref);
    if (importStatus != noErr) {
        NSLog(@"[ZYJMQTTCFSocketTransport] Error while importing pkcs12 [%d]", (int)importStatus);
        return nil;
    }
    
    CFDictionaryRef identityDict = CFArrayGetValueAtIndex(keyref, 0);
    if (!identityDict) {
        NSLog(@"[ZYJMQTTCFSocketTransport] could not CFArrayGetValueAtIndex");
        return nil;
    }
    
    SecIdentityRef identityRef = (SecIdentityRef)CFDictionaryGetValue(identityDict,
                                                                      kSecImportItemIdentity);
    if (!identityRef) {
        NSLog(@"[ZYJMQTTCFSocketTransport] could not CFDictionaryGetValue");
        return nil;
    };
    
    SecCertificateRef cert = NULL;
    OSStatus status = SecIdentityCopyCertificate(identityRef, &cert);
    if (status != noErr) {
        NSLog(@"[ZYJMQTTCFSocketTransport] SecIdentityCopyCertificate failed [%d]", (int)status);
        return nil;
    }
    
    NSArray *clientCerts = @[(__bridge id)identityRef, (__bridge id)cert];
    return clientCerts;
}

@end
