//
//  NSNetServices.m
//  mySTEP
//
//  Created by Dr. H. Nikolaus Schaller on Sat Aug 20 2005.
//  Copyright (c) 2005 DSITRI.
//
//    This file is part of the mySTEP Library and is provided
//    under the terms of the GNU Library General Public License.
//

#import "Foundation/Foundation.h"
#import "Foundation/NSNetServices.h"
#import "Foundation/NSStream.h"
#import "Foundation/NSObject.h"
#import "NSPrivate.h"

#include <sys/socket.h>
#include <netinet/in.h>
#include <fcntl.h>
#include <errno.h>

NSString *NSNetServicesErrorCode=@"NetServicesErrorCode";
NSString *NSNetServicesErrorDomain=@"NetServicesErrorDomain";

struct DNSHeader
{
	unsigned short identifier;
	unsigned short flags;
#define DNS_RESPONSE ((flags>>15)&1)
#define DNS_OPCODE ((flags>>13)&0xf)
#define DNS_AA ((flags>>15)&1)
	// usw.
	unsigned short qdcount;
	unsigned short ancount;
	unsigned short nscount;
	unsigned short arcount;
};

@implementation NSNetService

+ (NSData *) dataFromTXTRecordDictionary:(NSDictionary *) txt;
{
	NSMutableData *d=[NSMutableData dataWithCapacity:512];	// should not become larger!
	NSEnumerator *e=[txt keyEnumerator];
	NSString *key;
	while((key=[e nextObject]))
		{
		const char *k=[key UTF8String];
		unsigned int klen=strlen(k);
		id val=[txt objectForKey:key];
		const char *v;
		unsigned int len;
		unsigned char clen;
		if([val isKindOfClass:[NSData class]])
			v=[val bytes], len=[val length];
		else
			v=[val UTF8String], len=strlen(v);
		clen=klen;
		if(len > 254)
			return nil;	// should we raise exception?
		if(len>0)
			clen+=1+len;
		// check for overflow!
		[d appendBytes:&clen length:1];	// record length
		[d appendBytes:k length:klen];
		if(len > 0)
			{
			[d appendBytes:"=" length:1];
			[d appendBytes:v length:len];
			}
		}
	if([d length] == 0)
		{
		static char null=0;
		[d appendBytes:&null length:1];	// make empty record (key length=0)
		}
	else if([d length] > 512)
		return nil; // became too long
	return d;
}

+ (NSDictionary *) dictionaryFromTXTRecordData:(NSData *) txt;
{
	NSMutableDictionary *dict=[NSMutableDictionary dictionaryWithCapacity:10];
	const unsigned char *bytes=[txt bytes];
	const unsigned char *end=bytes+[txt length];
	while(bytes < end)
		{
		unsigned int len=*bytes++;
		const unsigned char *kp=bytes;	// start of key
		NSString *key;
		NSData *val;
		while(len > 0 && *bytes != '=')
			bytes++, len--;
		key=[[NSString _stringWithUTF8String:(char *) kp length:bytes-kp] lowercaseString];	// key isn't case sensitive
#if 1
		NSLog(@"key=%@ len=%d", key, bytes-kp);
#endif
		if(len > 0 && *bytes == '=')
			{ // with value
			val=[NSData dataWithBytes:++bytes length:--len];	// value starts behind =
			bytes+=len;	// skip all
			}
		else
			{ // no = i.e. a boolean value
			val=[NSData data];	// use empty value but present - should be interpreted as 'true' for a boolean value
			}
		if([key length] > 0 && ![dict objectForKey:key])	// ignore empty and duplicates
			[dict setObject:val forKey:key];	// store
		}
	return dict;
}

- (void) _notifyErrorDomain:(int) domain code:(int) code
{
	if([_delegate respondsToSelector:@selector(netService:didNotResolve:)])
		[_delegate netService:self didNotResolve:[NSDictionary dictionaryWithObjectsAndKeys:
			[NSNumber numberWithInt:code], NSNetServicesErrorCode,
			[NSNumber numberWithInt:domain], NSNetServicesErrorDomain,
			nil]];
}

static NSString *parseDomainName(unsigned char *buffer, int len, unsigned int *pos)
{
	int clen;
	NSString *domain=@"";
	while(*pos < len && (clen=buffer[(*pos)++]) > 0)
		{ // get next component
#if 0
		NSLog(@"pos=%d len=%d clen=%d", *pos, len, clen);
#endif
		domain=[domain stringByAppendingFormat:@"%@.", [NSString _stringWithUTF8String:(char *)(buffer+*pos) length:clen]];
		(*pos)+=clen;
		}
	return domain;
}

static unsigned short parseShort(unsigned char *buffer, int len, unsigned int *pos)
{
	unsigned short val=0;
	if(*pos < len-2)
		{
		val=buffer[(*pos)++]<<8;	// Bigendian network order
		val+=buffer[(*pos)++];
		}
	return val;
}

static unsigned long parseLong(unsigned char *buffer, int len, unsigned int *pos)
{
	unsigned long val=0;
	if(*pos < len-4)
		{
		val=buffer[(*pos)++]<<24;	// Bigendian network order
		val+=buffer[(*pos)++]<<16;
		val+=buffer[(*pos)++]<<8;
		val+=buffer[(*pos)++];
		}
	return val;
}

- (void) stream:(NSStream *) stream handleEvent:(NSStreamEvent) event;
{
#if 1
	NSLog(@"NSNetService stream:%@ handleEvent:%d", stream, event);
#endif
	if(stream == _inputStream)
		{
		switch(event)
			{
			case NSStreamEventOpenCompleted:
				return;
			case NSStreamEventHasBytesAvailable:
				{
					int len;
					// FIXME: static/malloc? - what is the maximum packet size we expect? 512 or 9000 or 65535?
					unsigned char buffer[512];
					int i;
					unsigned int pos;
#if 0
					NSLog(@"looks like a packet arrived");
#endif
					len=[_inputStream read:buffer maxLength:sizeof(buffer)];
#if 1
					NSLog(@"len=%d data=%@", len, [NSData dataWithBytesNoCopy:buffer length:len freeWhenDone:NO]);
#endif
					if(len < sizeof(struct DNSHeader))
						return;	// ignore
					((struct DNSHeader *)buffer)->identifier=NSSwapBigShortToHost(((struct DNSHeader *)buffer)->identifier);
					((struct DNSHeader *)buffer)->flags=NSSwapBigShortToHost(((struct DNSHeader *)buffer)->flags);
					((struct DNSHeader *)buffer)->qdcount=NSSwapBigShortToHost(((struct DNSHeader *)buffer)->qdcount);
					((struct DNSHeader *)buffer)->ancount=NSSwapBigShortToHost(((struct DNSHeader *)buffer)->ancount);
					((struct DNSHeader *)buffer)->nscount=NSSwapBigShortToHost(((struct DNSHeader *)buffer)->nscount);
					((struct DNSHeader *)buffer)->arcount=NSSwapBigShortToHost(((struct DNSHeader *)buffer)->arcount);
#if 1
					NSLog(@"%d question records", ((struct DNSHeader *)buffer)->qdcount);
#endif
					pos=sizeof(struct DNSHeader);
					for(i=0; i<((struct DNSHeader *)buffer)->qdcount; i++)
						{ // process question records
						NSString *qname;
						unsigned short qtype;
						unsigned short qclass;
						qname=parseDomainName(buffer, len, &pos);
#if 1
						NSLog(@"qname=%@", qname);
#endif
						qtype=parseShort(buffer, len, &pos);
						qclass=parseShort(buffer, len, &pos);
						}
#if 1
					NSLog(@"%d answer records", ((struct DNSHeader *)buffer)->ancount);
#endif
					for(i=0; i<((struct DNSHeader *)buffer)->ancount; i++)
						{ // process answer records
						NSString *name;
						unsigned short type;
						unsigned short class;
						unsigned long ttl;
						unsigned short rdlen;
						NSData *record;
						name=parseDomainName(buffer, len, &pos);						
#if 1
						NSLog(@"name=%@", name);
#endif
						type=parseShort(buffer, len, &pos);
						class=parseShort(buffer, len, &pos);
						ttl=parseLong(buffer, len, &pos);
						rdlen=parseShort(buffer, len, &pos);
#if 1
						NSLog(@"type=%d", type);
						NSLog(@"class=%d", class);
						NSLog(@"ttl=%d", ttl);
						NSLog(@"rdlen=%d", rdlen);
#endif
						if(rdlen+pos <= len)
							{
							record=[NSData dataWithBytesNoCopy:(char *)(buffer+pos) length:rdlen freeWhenDone:NO];
							pos+=rdlen;
							}
						else
							record=nil;	// some error
						if(type == 33)
							{
							// process - otherwise ignore
							}
#if 1
						NSLog(@"TXT record=%@", record);
						NSLog(@"dict=%@", [isa dictionaryFromTXTRecordData:record]);
#endif
						}
				// if packet received
				// parse and decode
				// if monitoring: [_delegate netService:self didUpdateTXTRecordData:data];
				// if yes, notify [_delegate netServiceDidResolveAddress:self];
				// [_timer release], _timer=nil;
					return;
				}
			case NSStreamEventEndEncountered:
			case NSStreamEventHasSpaceAvailable:
			case NSStreamEventNone:
			default:
				break;
			}
		}
	NSLog(@"An error %@ occurred on the event %08x of stream %@ of %@", [stream streamError], event, stream, self);
}

- (NSArray *) addresses; { return _addresses; }
- (id) delegate; { return _delegate; }
- (NSString *) domain; { return _domain; }

- (BOOL) getInputStream:(NSInputStream **) input outputStream:(NSOutputStream **) output;
{
	if([_addresses count] == 0)
		return NO;	// not yet resolved successfully
	// FIXME: get host&port from found service(s)
	[NSStream getStreamsToHost:[NSHost hostWithName:_hostName] port:12345 inputStream:input outputStream:output];
	return *input && *output;
}

- (NSString *) hostName; { return _hostName; }

- (id) initWithDomain:(NSString *) domain type:(NSString *) type name:(NSString *) name;
{ // adaequate to resolve a net service
	return [self initWithDomain:domain type:type name:name port:-1];
}

- (id) initWithDomain:(NSString *) domain type:(NSString *) type name:(NSString *) name port:(int) port;
{ // adaequate to publish a net service
	if((self=[super init]))
		{
		int s;	// the socket
		struct sockaddr_in saddr;	// socket address
		struct ip_mreq mc;			// socket options
		int flag = 1;
		int ittl = 255;
		char ttl = 255;
#if 1
		NSLog(@"%@ initWithDomain:%@ type:%@ name:%@ port:%d", NSStringFromClass(isa), domain, type, name, port);
#endif
		_addresses=[[NSMutableArray alloc] initWithCapacity:10];	// store NSData objects with struct sockaddr
		if([domain length] == 0)
			_domain=@"local.";	// use local domain
		else
			_domain=[domain retain];
		_type=[type retain];
		_name=[name retain];
		_port=port;
		if(port < 0)
			{ // we want to resolve
			saddr.sin_family = AF_INET;
			saddr.sin_port = htons(5353);
			saddr.sin_addr.s_addr = 0;
			}
		else
			{ // we (probably) want to publish a service
			saddr.sin_family = AF_INET;
			saddr.sin_port = htons(5353);
			saddr.sin_addr.s_addr = 0;
			}
		s=socket(saddr.sin_family, SOCK_DGRAM, PF_UNSPEC);
		if(s < 0)
			{
			NSLog(@"NSNetService: could not create multicast socket due to %s", strerror(errno));
			[self release];
			return nil;
			}
#ifdef SO_REUSEPORT
		setsockopt(s, SOL_SOCKET, SO_REUSEPORT, (char*)&flag, sizeof(flag));	// reuse port
#endif
		setsockopt(s, SOL_SOCKET, SO_REUSEADDR, (char*)&flag, sizeof(flag));	// reuse address
		if(bind(s, (struct sockaddr *) &saddr, sizeof(saddr)) != 0)
			{
			close(s); 
			NSLog(@"NSNetService: could not bind multicast socket due to %s", strerror(errno));
			[self release];
			return nil;
			}
		inet_aton("224.0.0.251", &mc.imr_multiaddr);
		mc.imr_interface.s_addr = INADDR_ANY;
		setsockopt(s, IPPROTO_IP, IP_ADD_MEMBERSHIP, &mc, sizeof(mc)); 
		setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, &ttl, sizeof(ttl));
		setsockopt(s, IPPROTO_IP, IP_MULTICAST_TTL, &ittl, sizeof(ittl));
		flag =  fcntl(s, F_GETFL, 0);
		flag |= O_NONBLOCK;
		fcntl(s, F_SETFL, flag);
		// shouldn't we check for errors?
		_inputStream=[[NSInputStream alloc] _initWithFileDescriptor:s];
		_outputStream=[[NSOutputStream alloc] _initWithFileDescriptor:s];
		[_inputStream setDelegate:self];	// make us receive packet notifications
		[self scheduleInRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
		[_inputStream open];
		[_outputStream open];
		}
	return self;
}

- (void) dealloc;
{
	[self removeFromRunLoop:[NSRunLoop currentRunLoop] forMode:NSDefaultRunLoopMode];
//	[_inputStream close];
//	[_outputStream close];
	[_inputStream release];
	[_outputStream release];
	[_addresses release];
	[_domain release];
	[_type release];
	[_hostName release];
	[_txt release];
	[_timer release];
	[super dealloc];
}

- (NSString *) name; { return _name; }

- (NSString *) protocolSpecificInformation;
{ // deprecated
	NIMP;
	return nil;
}

- (void) publish;
{
	if(_timer)
		return;	// should raise exception because we are resolving
	if(_isPublishing)
		return;	// already
	if(_port < 0)
		return;	// initialized for resolving
	[_delegate netServiceWillPublish:self];
	// send publish record
	// handle timers (?)
}

- (void) removeFromRunLoop:(NSRunLoop *) loop forMode:(NSString *) mode;
{
	[_inputStream removeFromRunLoop:loop forMode:mode];
}

- (void) resolve;
{ // deprecated!
	[self resolveWithTimeout:5.0];
}

- (void) _resolveTimedOut:(NSTimer *) timer;
{
	[_timer release];
	_timer=nil;
	[self _notifyErrorDomain:0 code:NSNetServicesTimeoutError];
}

- (void) resolveWithTimeout:(NSTimeInterval) interval;
{
#if 1
	NSLog(@"NSNetService -resolveWithTimeout:%.0lf", interval);
#endif
	if(_isPublishing)
		return;	// raise exception
	if(_timer)
		return;	// already resolving
	if(_port > 0)
		return;	// initialized for publishing
	_timer=[[NSTimer scheduledTimerWithTimeInterval:interval target:self selector:@selector(_resolveTimedOut:) userInfo:nil repeats:NO] retain];
	[_delegate netServiceWillResolve:self];
	// send resolve request
}

- (void) scheduleInRunLoop:(NSRunLoop *) loop forMode:(NSString *) mode;
{
#if 1
	NSLog(@"schedule %@ in %@", _inputStream, mode);
#endif
	[_inputStream scheduleInRunLoop:loop forMode:mode];
}

- (void) setDelegate:(id) obj;
{
	_delegate=obj;
}

- (void) setProtocolSpecificInformation:(NSString *) info;
{
	// deprecated
	NIMP;
}

- (BOOL) setTXTRecordData:(NSData *) txt;
{
	// check for valid data
	ASSIGN(_txt, txt);
	return YES;
}

- (void) startMonitoring;
{
#if 1
	NSLog(@"NSNetService -startMonitoring");
#endif
	_isMonitoring=YES;
}

- (void) stop;
{
	_isPublishing=NO;
	if(_timer)
		{
		[_timer release];
		_timer=nil;
		[self _notifyErrorDomain:0 code:NSNetServicesCancelledError];
		}
	[_delegate netServiceDidStop:self];
}

- (void) stopMonitoring;
{
	_isMonitoring=NO;
}

- (NSString *) type; { return _type; }

- (NSData *) TXTRecordData; { return _txt; }

@end

@implementation NSNetServiceBrowser

- (void) netService:(NSNetService *) sender didNotPublish:(NSDictionary *) error; {} // ignore since we never publish
- (void) netServiceDidPublish:(NSNetService *) sender; {} // ignore since we never publish
- (void) netServiceWillPublish:(NSNetService *) sender; {} // ignore since we never publish

- (void) netService:(NSNetService *) sender didNotResolve:(NSDictionary *) error;
{
}

- (void) netServiceDidResolveAddress:(NSNetService *) sender;
{
	//
}

- (void) netServiceDidStop:(NSNetService *) sender;
{
	return;	// ignore
}

- (void) netService:(NSNetService *) sender didUpdateTXTRecordData:(NSData *) txt;
{
}

- (void) netServiceWillResolve:(NSNetService *) sender;
{
#if 1
	NSLog(@"netServiceWillResolve:%@", sender);
#endif
	[_delegate netServiceBrowserWillSearch:self];	
}

- (id) delegate; { return _delegate; }

- (id) init;
{
	if((self=[super init]))
		{
#if 1
		NSLog(@"%@ %@", NSStringFromClass(isa), NSStringFromSelector(_cmd));
#endif
		}
	return self;
}

- (void) dealloc;
{
	[self stop];
	[super dealloc];
}

- (void) removeFromRunLoop:(NSRunLoop *) loop forMode:(NSString *) mode;
{
	NIMP;
}

- (void) scheduleInRunLoop:(NSRunLoop *) loop forMode:(NSString *) mode;
{
	NIMP;
}

- (void) searchForBrowsableDomains;
{
	[self searchForServicesOfType:@"browseable" inDomain:@""];
}

- (void) searchForRegistrationDomains;
{
	[self searchForServicesOfType:@"registration" inDomain:@""];
}

- (void) searchForServicesOfType:(NSString *) type inDomain:(NSString *) domain;
{
#if 1
	NSLog(@"NSNetService -searchForServicesOfType:%@ inDomain:%@", type, domain);
#endif
	_netService=[[NSNetService alloc] initWithDomain:domain type:type name:@"?"];
	[_netService setDelegate:self];	// make us receive notifications
	[_netService startMonitoring];	// FIXME: do we need that?
	[_netService resolveWithTimeout:30.0];
}

- (void) setDelegate:(id) obj;
{
	_delegate=obj;
}

- (void) stop;
{
	if(_netService)
		{
		[_netService stop];
		[_netService release];
		_netService=nil;
		}
	[_delegate netServiceBrowserDidStopSearch:self];	
}

@end