//
//  XMPPSRVResolver.m
//
//  Originally created by Eric Chamberlain on 6/15/10.
//  Based on SRVResolver by Apple, Inc.
//

#import "XMPPSRVResolver.h"
#import "XMPPLogging.h"

//#warning Fix "dns.h" issue without resorting to this ugly hack.
// This is a hack to prevent OnionKit's clobbering of the actual system's <dns.h>
//#include "/usr/include/dns.h"

#include <dns_util.h>
#include <stdlib.h>
#import <dns_sd.h>

#if ! __has_feature(objc_arc)
#warning This file must be compiled with ARC. Use -fobjc-arc flag (or convert project to ARC).
#endif

NSString *const XMPPSRVResolverErrorDomain = @"XMPPSRVResolverErrorDomain";

// Log levels: off, error, warn, info, verbose
#if DEBUG
  static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN; // | XMPP_LOG_FLAG_TRACE;
#else
  static const int xmppLogLevel = XMPP_LOG_LEVEL_WARN;
#endif

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@interface XMPPSRVRecord ()

@property(nonatomic, assign) NSUInteger srvResultsIndex;
@property(nonatomic, assign) NSUInteger sum;

- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord;

@end

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@interface XMPPSRVResolver ()
{
#if __has_feature(objc_arc_weak)
    __weak id<XMPPSRVResolverDelegate> delegate;
#else
    __unsafe_unretained id<XMPPSRVResolverDelegate> delegate;
#endif
    
    dispatch_queue_t delegateQueue;
    
    dispatch_queue_t resolverQueue;
    void *resolverQueueTag;
    
    __strong NSString *srvName;
    NSTimeInterval timeout;
    
    BOOL resolveInProgress;
    
    NSMutableArray *results;
    DNSServiceRef sdRef;
    
    int sdFd;
    dispatch_source_t sdReadSource;
    dispatch_source_t timeoutTimer;
}

@end

@implementation XMPPSRVResolver

- (instancetype)initWithDelegate:(id<XMPPSRVResolverDelegate>)aDelegate
                   delegateQueue:(dispatch_queue_t)dq
                   resolverQueue:(nullable dispatch_queue_t)rq {
	NSParameterAssert(aDelegate != nil);
	NSParameterAssert(dq != NULL);
	
	if ((self = [super init]))
	{
		XMPPLogTrace();
		
		delegate = aDelegate;
		delegateQueue = dq;
		
		#if !OS_OBJECT_USE_OBJC
		dispatch_retain(delegateQueue);
		#endif

		if (rq)
		{
			resolverQueue = rq;
			#if !OS_OBJECT_USE_OBJC
			dispatch_retain(resolverQueue);
			#endif
		}
		else
		{
			resolverQueue = dispatch_queue_create("XMPPSRVResolver", NULL);
		}
		
		resolverQueueTag = &resolverQueueTag;
		dispatch_queue_set_specific(resolverQueue, resolverQueueTag, resolverQueueTag, NULL);
		
		results = [[NSMutableArray alloc] initWithCapacity:2];
	}
	return self;
}

- (void)dealloc
{
	XMPPLogTrace();
	
    [self stop];
	
	#if !OS_OBJECT_USE_OBJC
	if (resolverQueue)
		dispatch_release(resolverQueue);
	#endif
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Properties
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@dynamic srvName;
@dynamic timeout;

- (NSString *)srvName
{
	__block NSString *result = nil;
	
	dispatch_block_t block = ^{
		result = [self->srvName copy];
	};
	
	if (dispatch_get_specific(resolverQueueTag))
		block();
	else
		dispatch_sync(resolverQueue, block);
	
	return result;
}

- (NSTimeInterval)timeout
{
	__block NSTimeInterval result = 0.0;
	
	dispatch_block_t block = ^{
		result = self->timeout;
	};
	
	if (dispatch_get_specific(resolverQueueTag))
		block();
	else
		dispatch_sync(resolverQueue, block);
	
	return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Private Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)sortResults
{
	NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue");
	
	XMPPLogTrace();
	
	// Sort results
	NSMutableArray *sortedResults = [NSMutableArray arrayWithCapacity:[results count]];
	
	// Sort the list by priority (lowest number first)
	[results sortUsingSelector:@selector(compareByPriority:)];
	
	/* From RFC 2782
	 * 
	 * For each distinct priority level
	 * While there are still elements left at this priority level
	 * 
	 * Select an element as specified above, in the
	 * description of Weight in "The format of the SRV
	 * RR" Section, and move it to the tail of the new
	 * list.
	 * 
	 * The following algorithm SHOULD be used to order
	 * the SRV RRs of the same priority:
	 */
	
	NSUInteger srvResultsCount;
	
	while ([results count] > 0)
	{
		srvResultsCount = [results count];
		
		if (srvResultsCount == 1)
		{
			XMPPSRVRecord *srvRecord = results[0];
			
			[sortedResults addObject:srvRecord];
			[results removeObjectAtIndex:0];
		}
		else // (srvResultsCount > 1)
		{
			// more than two records so we need to sort
			
			/* To select a target to be contacted next, arrange all SRV RRs
			 * (that have not been ordered yet) in any order, except that all
			 * those with weight 0 are placed at the beginning of the list.
			 * 
			 * Compute the sum of the weights of those RRs, and with each RR
			 * associate the running sum in the selected order.
			 */
			
			NSUInteger runningSum = 0;
			NSMutableArray *samePriorityRecords = [NSMutableArray arrayWithCapacity:srvResultsCount];
			
			XMPPSRVRecord *srvRecord = results[0];
			
			NSUInteger initialPriority = srvRecord.priority;
			NSUInteger index = 0;
			
			do
			{
				if (srvRecord.weight == 0)
				{
					// add to front of array
					[samePriorityRecords insertObject:srvRecord atIndex:0];
					
					srvRecord.srvResultsIndex = index;
					srvRecord.sum = 0;
				}
				else
				{
					// add to end of array and update the running sum
					[samePriorityRecords addObject:srvRecord];
					
					runningSum += srvRecord.weight;
					
					srvRecord.srvResultsIndex = index;
					srvRecord.sum = runningSum;
				}
				
				if (++index < srvResultsCount)
				{
					srvRecord = results[index];
				}
				else
				{
					srvRecord = nil;
				}
				
			} while(srvRecord && (srvRecord.priority == initialPriority));
			
			/* Then choose a uniform random number between 0 and the sum computed
			 * (inclusive), and select the RR whose running sum value is the
			 * first in the selected order which is greater than or equal to
			 * the random number selected.
			 */
			
			NSUInteger randomIndex = arc4random() % (runningSum + 1);
			
			for (srvRecord in samePriorityRecords)
			{
				if (srvRecord.sum >= randomIndex)
				{
					/* The target host specified in the
					 * selected SRV RR is the next one to be contacted by the client.
					 * Remove this SRV RR from the set of the unordered SRV RRs and
					 * apply the described algorithm to the unordered SRV RRs to select
					 * the next target host.  Continue the ordering process until there
					 * are no unordered SRV RRs.  This process is repeated for each
					 * Priority.
					 */
					
					[sortedResults addObject:srvRecord];
					[results removeObjectAtIndex:srvRecord.srvResultsIndex];
					
					break;
				}
			}
		}
	}
	
	results = sortedResults;
	
	XMPPLogVerbose(@"%@: Sorted results:\n%@", THIS_FILE, results);
}

- (void)succeed
{
    NSParameterAssert(delegate != nil);
    NSParameterAssert(delegateQueue != nil);
	NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue");
	
	XMPPLogTrace();
    
    if (!delegate || !delegateQueue) {
        XMPPLogError(@"%@: No delegate or queue set for SRV resolver.", THIS_FILE);
        return;
    }
	
	[self sortResults];
	
	id theDelegate = delegate;
	NSArray *records = [results copy];
	
	dispatch_async(delegateQueue, ^{ @autoreleasepool {
		
		SEL selector = @selector(xmppSRVResolver:didResolveRecords:);
		
		if ([theDelegate respondsToSelector:selector])
		{
			[theDelegate xmppSRVResolver:self didResolveRecords:records];
		}
		else
		{
			XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector));
		}
		
	}});
	
	[self stop];
}

- (void)failWithError:(NSError *)error
{
	NSAssert(dispatch_get_specific(resolverQueueTag), @"Invoked on incorrect queue");
	
	XMPPLogTrace2(@"%@: %@ %@", THIS_FILE, THIS_METHOD, error);
	
	id theDelegate = delegate;
	
    if (delegateQueue != NULL)
	{
		dispatch_async(delegateQueue, ^{ @autoreleasepool {
			
			SEL selector = @selector(xmppSRVResolver:didNotResolveDueToError:);
			
			if ([theDelegate respondsToSelector:selector])
			{
				[theDelegate xmppSRVResolver:self didNotResolveDueToError:error];
			}
			else
			{
				XMPPLogWarn(@"%@: delegate doesn't implement %@", THIS_FILE, NSStringFromSelector(selector));
			}
			
		}});
	}
	
	[self stop];
}

- (void)failWithDNSError:(DNSServiceErrorType)sdErr
{
	XMPPLogTrace2(@"%@: %@ %i", THIS_FILE, THIS_METHOD, (int)sdErr);
	
	[self failWithError:[NSError errorWithDomain:XMPPSRVResolverErrorDomain code:sdErr userInfo:nil]];
}

- (XMPPSRVRecord *)processRecord:(const void *)rdata length:(uint16_t)rdlen
{
	XMPPLogTrace();
	
	// Note: This method is almost entirely from Apple's sample code.
	// 
	// Otherwise there would be a lot more comments and explanation...
	
	if (rdata == NULL)
	{
		XMPPLogWarn(@"%@: %@ - rdata == NULL", THIS_FILE, THIS_METHOD);
		return nil;
	}
	
	// Rather than write a whole bunch of icky parsing code, I just synthesise
	// a resource record and use <dns_util.h>.
	
	XMPPSRVRecord *result = nil;
	
	NSMutableData *         rrData;
	dns_resource_record_t * rr;
	uint8_t                 u8;   // 1 byte
	uint16_t                u16;  // 2 bytes
	uint32_t                u32;  // 4 bytes
	
	rrData = [NSMutableData dataWithCapacity:(1 + 2 + 2 + 4 + 2 + rdlen)];
	
	u8 = 0;
	[rrData appendBytes:&u8 length:sizeof(u8)];
	u16 = htons(kDNSServiceType_SRV);
	[rrData appendBytes:&u16 length:sizeof(u16)];
	u16 = htons(kDNSServiceClass_IN);
	[rrData appendBytes:&u16 length:sizeof(u16)];
	u32 = htonl(666);
	[rrData appendBytes:&u32 length:sizeof(u32)];
	u16 = htons(rdlen);
	[rrData appendBytes:&u16 length:sizeof(u16)];
	[rrData appendBytes:rdata length:rdlen];
	
	// Parse the record.
	
	rr = dns_parse_resource_record([rrData bytes], (uint32_t) [rrData length]);
    if (rr != NULL)
	{
        NSString *target;
        
        target = [NSString stringWithCString:rr->data.SRV->target encoding:NSASCIIStringEncoding];
        if (target != nil)
		{
			UInt16 priority = rr->data.SRV->priority;
			UInt16 weight   = rr->data.SRV->weight;
			UInt16 port     = rr->data.SRV->port;
			
			result = [XMPPSRVRecord recordWithPriority:priority weight:weight port:port target:target];
        }
		
        dns_free_resource_record(rr);
    }
	
	return result;
}

static void QueryRecordCallback(DNSServiceRef       sdRef,
                                DNSServiceFlags     flags,
                                uint32_t            interfaceIndex,
                                DNSServiceErrorType errorCode,
                                const char *        fullname,
                                uint16_t            rrtype,
                                uint16_t            rrclass,
                                uint16_t            rdlen,
                                const void *        rdata,
                                uint32_t            ttl,
                                void *              context)
{
	// Called when we get a response to our query.  
	// It does some preliminary work, but the bulk of the interesting stuff 
	// is done in the processRecord:length: method.
	
	XMPPSRVResolver *resolver = (__bridge XMPPSRVResolver *)context;
	
	NSCAssert(dispatch_get_specific(resolver->resolverQueueTag), @"Invoked on incorrect queue");
    
	XMPPLogCTrace();
	
	if (!(flags & kDNSServiceFlagsAdd))
	{
		// If the kDNSServiceFlagsAdd flag is not set, the domain information is not valid.
		return;
    }

    if (errorCode == kDNSServiceErr_NoError &&
        rrtype == kDNSServiceType_SRV)
    {
        XMPPSRVRecord *record = [resolver processRecord:rdata length:rdlen];
        if (record)
        {
            [resolver->results addObject:record];
        }

        if ( ! (flags & kDNSServiceFlagsMoreComing) )
        {
            [resolver succeed];
        }    
    }
    else
    {
        [resolver failWithDNSError:errorCode];
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Public Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

- (void)startWithSRVName:(NSString *)aSRVName timeout:(NSTimeInterval)aTimeout
{
	dispatch_block_t block = ^{ @autoreleasepool {
		
		if (self->resolveInProgress)
		{
			return;
		}
		
		XMPPLogTrace2(@"%@: startWithSRVName:%@ timeout:%f", THIS_FILE, aSRVName, aTimeout);
		
		// Save parameters
		
		self->srvName = [aSRVName copy];
		
		self->timeout = aTimeout;
		
		// Check parameters
		
		const char *srvNameCStr = [self->srvName cStringUsingEncoding:NSASCIIStringEncoding];
		if (srvNameCStr == NULL)
		{
			[self failWithDNSError:kDNSServiceErr_BadParam];
			return;
			
		}
		
		// Create DNS Service
		
		DNSServiceErrorType sdErr;
		sdErr = DNSServiceQueryRecord(&self->sdRef,                              // Pointer to unitialized DNSServiceRef
		                              kDNSServiceFlagsReturnIntermediates, // Flags
		                              kDNSServiceInterfaceIndexAny,        // Interface index
		                              srvNameCStr,                         // Full domain name
		                              kDNSServiceType_SRV,                 // rrtype
		                              kDNSServiceClass_IN,                 // rrclass
		                              QueryRecordCallback,                 // Callback method
		                              (__bridge void *)self);              // Context pointer
		
		if (sdErr != kDNSServiceErr_NoError)
		{
			[self failWithDNSError:sdErr];
			return;
		}
		
		// Extract unix socket (so we can poll for events)
		
		self->sdFd = DNSServiceRefSockFD(self->sdRef);
		if (self->sdFd < 0)
		{
			// Todo...
		}
		
		// Create GCD read source for sd file descriptor
		
		self->sdReadSource = dispatch_source_create(DISPATCH_SOURCE_TYPE_READ, self->sdFd, 0, self->resolverQueue);
		
		dispatch_source_set_event_handler(self->sdReadSource, ^{ @autoreleasepool {
			
			XMPPLogVerbose(@"%@: sdReadSource_eventHandler", THIS_FILE);
			
			// There is data to be read on the socket (or an error occurred).
			// 
			// Invoking DNSServiceProcessResult will invoke our QueryRecordCallback,
			// the callback we set when we created the sdRef.
			
			DNSServiceErrorType dnsErr = DNSServiceProcessResult(self->sdRef);
			if (dnsErr != kDNSServiceErr_NoError)
			{
				[self failWithDNSError:dnsErr];
			}
			
		}});
		
		#if !OS_OBJECT_USE_OBJC
		dispatch_source_t theSdReadSource = sdReadSource;
		#endif
		DNSServiceRef theSdRef = self->sdRef;
		
		dispatch_source_set_cancel_handler(self->sdReadSource, ^{ @autoreleasepool {
			
			XMPPLogVerbose(@"%@: sdReadSource_cancelHandler", THIS_FILE);
			
			#if !OS_OBJECT_USE_OBJC
			dispatch_release(theSdReadSource);
			#endif
			DNSServiceRefDeallocate(theSdRef);
			
		}});
		
		dispatch_resume(self->sdReadSource);
		
		// Create timer (if requested timeout > 0)
		
		if (self->timeout > 0.0)
		{
			self->timeoutTimer = dispatch_source_create(DISPATCH_SOURCE_TYPE_TIMER, 0, 0, self->resolverQueue);
			
			dispatch_source_set_event_handler(self->timeoutTimer, ^{ @autoreleasepool {
				
				NSString *errMsg = @"Operation timed out";
				NSDictionary *userInfo = @{NSLocalizedDescriptionKey : errMsg};
				
				NSError *err = [NSError errorWithDomain:XMPPSRVResolverErrorDomain code:0 userInfo:userInfo];
				
				[self failWithError:err];
				
			}});
			
			dispatch_time_t tt = dispatch_time(DISPATCH_TIME_NOW, (self->timeout * NSEC_PER_SEC));
			
			dispatch_source_set_timer(self->timeoutTimer, tt, DISPATCH_TIME_FOREVER, 0);
			dispatch_resume(self->timeoutTimer);
		}
		
		self->resolveInProgress = YES;
	}};
	
	if (dispatch_get_specific(resolverQueueTag))
		block();
	else
		dispatch_async(resolverQueue, block);
}

- (void)stop
{
	dispatch_block_t block = ^{ @autoreleasepool {
		
		XMPPLogTrace();
		
		self->delegate = nil;
		if (self->delegateQueue)
		{
			#if !OS_OBJECT_USE_OBJC
			dispatch_release(delegateQueue);
			#endif
			self->delegateQueue = NULL;
		}
		
		[self->results removeAllObjects];
		
		if (self->sdReadSource)
		{
			// Cancel the readSource.
			// It will be released from within the cancel handler.
			dispatch_source_cancel(self->sdReadSource);
			self->sdReadSource = NULL;
			self->sdFd = -1;
			
			// The sdRef will be deallocated from within the cancel handler too.
			self->sdRef = NULL;
		}
		
		if (self->timeoutTimer)
		{
			dispatch_source_cancel(self->timeoutTimer);
			#if !OS_OBJECT_USE_OBJC
			dispatch_release(timeoutTimer);
			#endif
			self->timeoutTimer = NULL;
		}
		
		self->resolveInProgress = NO;
	}};
	
	if (dispatch_get_specific(resolverQueueTag))
		block();
	else
		dispatch_sync(resolverQueue, block);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark Utility Methods
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

+ (NSString *)srvNameFromXMPPDomain:(NSString *)xmppDomain
{
    NSParameterAssert(xmppDomain != nil);
	if (xmppDomain == nil)
		return nil;
	else
		return [NSString stringWithFormat:@"_xmpp-client._tcp.%@", xmppDomain];
}

@end

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
#pragma mark -
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

@implementation XMPPSRVRecord

@synthesize priority;
@synthesize weight;
@synthesize port;
@synthesize target;

@synthesize sum;
@synthesize srvResultsIndex;


+ (XMPPSRVRecord *)recordWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t
{
	return [[XMPPSRVRecord alloc] initWithPriority:p1 weight:w port:p2 target:t];
}

- (id)initWithPriority:(UInt16)p1 weight:(UInt16)w port:(UInt16)p2 target:(NSString *)t
{
	if ((self = [super init]))
	{
		priority = p1;
		weight   = w;
		port     = p2;
		target   = [t copy];
		
		sum = 0;
		srvResultsIndex = 0;
	}
	return self;
}


- (NSString *)description
{
	return [NSString stringWithFormat:@"<%@:%p target(%@) port(%hu) priority(%hu) weight(%hu)>",
			NSStringFromClass([self class]), self, target, port, priority, weight];
}

- (NSComparisonResult)compareByPriority:(XMPPSRVRecord *)aRecord
{
	UInt16 mPriority = self.priority;
	UInt16 aPriority = aRecord.priority;
	
	if (mPriority < aPriority)
		return NSOrderedAscending;
	
	if (mPriority > aPriority)
		return NSOrderedDescending;
	
	return NSOrderedSame;
}

@end
