#include <stdio.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include "error.h"
#include "tracker.h"
#include "gamemem.h"

#define assert(condition) ((condition) ? ((void) 0) : PrintConsoleString("%s@%s:%s",#condition,__FILE__, __LINE__))

#define kMaxLocalGames 32
int gNumLocalGames=0;
int gLocalGameListUpdate=0;
tGameListEntry gLocalGames[kMaxLocalGames];

static void
MyResolveCallback(CFNetServiceRef service, CFStreamError* error, void* info);

#define kServiceType			CFSTR("_redline._udp.")
#define kMyDefaultDomain 		CFSTR("local.")

CFMutableArrayRef		gServiceArrayRef;
CFMutableDictionaryRef 	gServiceDictionary;
CFNetServiceBrowserRef 	gServiceBrowserRef = NULL;
CFNetServiceRef 		gRegisteredService = NULL;
CFNetServiceRef 		gServiceBeingResolved = NULL;
UInt32					gNextNameIndexToResolve = 0;
CFStringRef 			gTextRecord;
Boolean					gDone;
Boolean					gContinue;
Boolean					gResolve;
Boolean					gBrowserTaskActive = FALSE;
Boolean					gResolveProcessActive = FALSE;
Boolean					gTaskRegistered	= FALSE;


void InitLocalTracker()
{
	OSStatus	err = noErr;
	
	gServiceDictionary = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, &kCFCopyStringDictionaryKeyCallBacks, NULL);
	if (gServiceDictionary == NULL)
	    err = coreFoundationUnknownErr;
	
	if (err == noErr)
	{
		gServiceArrayRef = CFArrayCreateMutable(kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
		if (gServiceDictionary == NULL)
		{
		    err = coreFoundationUnknownErr;
		}
	}
		
	//if (err == noErr)
	//	err = LoadNetServicesForCFM();
	//return err;

}

static void
MyCancelRegistration()
{
    if(gRegisteredService != NULL)
    {
	    CFNetServiceUnscheduleFromRunLoop(gRegisteredService, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
	    CFNetServiceSetClient(gRegisteredService, NULL, NULL);
	    CFNetServiceCancel(gRegisteredService);
	    CFRelease(gRegisteredService);
	    gRegisteredService = NULL;
	}
}

static void
MyRegisterCallback(CFNetServiceRef theService, CFStreamError* error, void* info)
{
    if (error->domain == kCFStreamErrorDomainNetServices)
    {
     	switch(error->error)
        {
	  		case kCFNetServicesErrorCollision:
	  
				/* Somebody else on the network has registered a service with the same name and type
				as the service we tried to register
				 */
				PrintConsoleString("A kCFNetServicesErrorCollision occured - will quit now\n");
				break;
				
			default:
				/*
					some other error occurred
				*/
				PrintConsoleString("Some other kCFNetServicesError occurred %d will quit now\n", error->error);
				break;
        }
       	// as an error occurred, clean up the CFNetServiceRef object
        MyCancelRegistration();
        // you don't really need to quit, but the following boolean will cause the main to quit.
        gDone = true;
    }

    return;
}

static void
MyCancelResolve()
{
    assert(gServiceBeingResolved != NULL);
    
    CFNetServiceUnscheduleFromRunLoop(gServiceBeingResolved, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    CFNetServiceSetClient(gServiceBeingResolved, NULL, NULL);
    CFNetServiceCancel(gServiceBeingResolved);
    CFRelease(gServiceBeingResolved);
    gServiceBeingResolved = NULL;

    return;
}


static void
MyResolveService(CFStringRef name, CFStringRef type, CFStringRef domain)
{
    CFNetServiceClientContext clientContext = { 0, NULL, NULL, NULL, NULL };
    CFStreamError error;

    assert(name != NULL);
    assert(type != NULL);
    assert(domain != NULL);
    
    if (gServiceBeingResolved)
    {
        /* This app only allows one resolve at a time, but CFNetServices places no restrictions on the number of
        simultaneous resolves.  Because most resolves will happen instantaneously after calling CFNetServiceResolve,
        if we end up canceling a previous resolve, it's probably because the previous service became unreachable. */
    
        PrintConsoleString("Resolve canceled\n");
        MyCancelResolve();
    }
    
    gServiceBeingResolved = CFNetServiceCreate(kCFAllocatorDefault, domain, type, name, 0);
    assert(gServiceBeingResolved != NULL);
    
    CFNetServiceSetClient(gServiceBeingResolved, MyResolveCallback, &clientContext);
    CFNetServiceScheduleWithRunLoop(gServiceBeingResolved, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);

    if (CFNetServiceResolve(gServiceBeingResolved, &error) == false)
    {
        // Something went wrong so lets clean up.
        CFNetServiceUnscheduleFromRunLoop(gServiceBeingResolved, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        CFNetServiceSetClient(gServiceBeingResolved, NULL, NULL);
        CFRelease(gServiceBeingResolved);
        gServiceBeingResolved = NULL;

        PrintConsoleString("CFNetServiceResolve returned (domain = %d, error = %ld)\n", error.domain, error.error);
    }

    return;
}

static void ContinueResolveProcess(void)
{
	CFStringRef	serviceNameRef;
	
	if (gDone == true)
	{
		gResolveProcessActive = FALSE;
	}
	else
	{	
		if (CFArrayGetCount(gServiceArrayRef) > gNextNameIndexToResolve)
		{
			(const void*)serviceNameRef = CFArrayGetValueAtIndex(gServiceArrayRef, gNextNameIndexToResolve++);
			MyResolveService(serviceNameRef, kServiceType, kMyDefaultDomain);
		}
		else
			gResolveProcessActive = FALSE;
	}
}


#define U32Swap(value) ((value)=EndianU32_BtoN(value))
#define U16Swap(value) ((value)=EndianU16_BtoN(value))

static void
MyResolveCallback(CFNetServiceRef service, CFStreamError* error, void* info)
{
    CFArrayRef 			addresses;
    CFStringRef 		addressString;
    CFStringRef 		serviceNameRef;
	sockaddr		*socketAddress;
	sockaddr_in 	*socketAddress_in;
    UInt32				inaddr;
	UInt16				port;
    char				servicename[64];
    CFIndex				namelen = sizeof(servicename);
    UInt32				index;

    /* In situations where the service you're resolving is advertising on multiple interfaces,
    like Ethernet and AirPort, your Resolve callback may be called twice, once for each IP address.
    Chances are that both of these IP addresses represent the same service running on the same machine,
    so we cancel the Resolve after getting the first callback because you only need one address to
    connect to the service.  However, it would also be possible that two different machines are
    advertising the same service name, with one being on Ethernet and one on AirPort.  In that
    situation, one of the machines will be unreachable, or more likly, everytime we call Resolve,
    we may connect to a different machine.  The odds of this happening are extremely small. */
    assert(service != NULL);
    
    addresses = CFNetServiceGetAddressing(service);
    assert(addresses != NULL);
    serviceNameRef = CFNetServiceGetName(service);
    assert(serviceNameRef != NULL);
	if (CFStringGetCString(serviceNameRef, servicename, namelen, kCFStringEncodingMacRoman))
	{		
		servicename[namelen]='\0';
		for(int i=0;i<gNumLocalGames;i++)
			if(_stricmp(servicename,gLocalGames[i].name)==0)
			{       
			    // Iterate through addresses until we find an IPv4 address
			    for (index = 0; index < CFArrayGetCount(addresses); index++)
			    {
			        socketAddress = (struct sockaddr *)CFDataGetBytePtr((__CFData*)CFArrayGetValueAtIndex(addresses, index));
			        if (socketAddress && socketAddress->sa_family == AF_INET)
			            break;
			    }
			        
			    if (socketAddress)
			    {
			        switch(socketAddress->sa_family)
			        {
			            case AF_INET:
					    	CFStringGetCString(serviceNameRef, servicename, namelen, kCFStringEncodingMacRoman);
					    	socketAddress_in = (struct sockaddr_in *)socketAddress;
					    	inaddr = *((UInt32*)(&socketAddress_in->sin_addr));
							port = *((UInt16*)(&socketAddress_in->sin_port));
							U32Swap(inaddr);
							U16Swap(port);
							sprintf(gLocalGames[i].host, "%d.%d.%d.%d:%d",inaddr>>24,(inaddr>>16)&0xFF,(inaddr>>8)&0xFF,inaddr&0xFF,port);
			                gLocalGames[i].loaded=true;
			                //PrintConsoleString("Got Host Info: %s",gLocalGames[i].host);
			                // Since we got an IPv4 address, this would be a good place to cancel the resolve.
							gLocalGameListUpdate++;
			    			MyCancelResolve();
			                return;
			                
			            case AF_INET6:
			            	PrintConsoleString("Resolver called with IPV6 address\n");
			                /* If we got here, it probably means that the "addresses" array only had one sockaddr in it and it
			                was IPv6. We don't cancel the resolve just yet since this sample expects to print out the IPV4 address.
			                Just continue and the resolver will call back with the IPV4 address instance. */
			                break;
			        }        
			    }
			}			    
    }

        
    // see if there are more entities to resolve
    ContinueResolveProcess();
    
    return;
}


static Boolean
MyRegisterService(CFStringRef name, CFStringRef type, CFStringRef domain, UInt32 port, CFStringRef txtRecord)
{    
    CFNetServiceClientContext context = {0, NULL, NULL, NULL, NULL };
    CFStreamError error;

    assert(name != NULL);
    assert(type != NULL);
    assert(domain != NULL);
    
    /* As an alternative to specifying a "name" to register, you could use an empty string like
    CFSTR(""), and that would cause the system to automatically substitute the "Computer Name"
    from the Sharing preference panel.  Another benefit of using an empty string is that the system
    will automatically handle name collisions for us by appending a digit to the end of the name,
    thus our Callback would never be called in the event of a name collision.
    
    Beginning with Mac OS X 10.2.4, using an empty string will even handle situations
    where the user changes the "Computer Name" in the preference panel. */

    gRegisteredService = CFNetServiceCreate(kCFAllocatorDefault, domain, type, name, port); 
    assert(gRegisteredService != NULL);

	// register the service asynchronously.
    CFNetServiceSetClient(gRegisteredService, MyRegisterCallback, &context);
    CFNetServiceScheduleWithRunLoop(gRegisteredService, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    gTaskRegistered = CFNetServiceRegister(gRegisteredService, &error);
    if (gTaskRegistered == FALSE)
    {
    	// a problem happened with calling CFNetServiceRegister so unschedule the service from
    	// the runloop
        CFNetServiceUnscheduleFromRunLoop(gRegisteredService, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        CFNetServiceSetClient(gRegisteredService, NULL, NULL);
        CFRelease(gRegisteredService);
        gRegisteredService = NULL;
    }
        
    return gTaskRegistered;
}

static void StartResolveProcess(void)
{
	CFStringRef	serviceNameRef;
	
	// Make sure that this process can't be entered multiple times
	if (gResolveProcessActive == FALSE)
	{
		gResolveProcessActive = TRUE;
		gNextNameIndexToResolve = 0;
		if (CFArrayGetCount(gServiceArrayRef) > gNextNameIndexToResolve)
		{
			(const void*)serviceNameRef = CFArrayGetValueAtIndex(gServiceArrayRef, gNextNameIndexToResolve++);
			MyResolveService(serviceNameRef, kServiceType, kMyDefaultDomain);
		}
		else
			gResolveProcessActive = FALSE;
	}
}

static void
MyAddService(CFNetServiceRef service, CFOptionFlags flags)
{
    int 		referenceCount;
    CFStringRef serviceNameRef;
    char		servicename[256];
    CFIndex		namelen = sizeof(servicename);
    Boolean		addName = FALSE;
        
    /* You should do reference counting of each service because if the computer has two network
    interfaces set up, like Ethernet and AirPort, you may get notified about the same service
    twice, once from each interface.  You probably don't want both items to be shown to the user. */

    assert(service != NULL);
        
    serviceNameRef = CFNetServiceGetName(service);
    
    assert(serviceNameRef != NULL);
    assert(gServiceDictionary != NULL);

	if (CFStringGetCString(serviceNameRef, servicename, namelen, kCFStringEncodingMacRoman))
	{		
		servicename[namelen]='\0';
		for(int i=0;i<gNumLocalGames;i++)
			if(_stricmp(servicename,gLocalGames[i].name)==0)
				return;
		
		strcpy(gLocalGames[gNumLocalGames].name,servicename);
		gLocalGames[gNumLocalGames].numPlayers=-1;
		gLocalGames[gNumLocalGames].loaded=false;
		gLocalGames[gNumLocalGames].mapRef=kFileErr;
		MyResolveService(serviceNameRef,kServiceType,kMyDefaultDomain);
		gNumLocalGames++;
		gLocalGameListUpdate++;
	}
}


static void
MyRemoveService(CFNetServiceRef service, CFOptionFlags flags)
{
	int			referenceCount;
	CFRange		range;
	CFIndex		index;
    CFStringRef serviceNameRef;
    char		servicename[256];
    CFIndex		namelen = sizeof servicename;
    CFIndex		result;
    
    assert(service != NULL);

    serviceNameRef = CFNetServiceGetName(service);
    
    assert(serviceNameRef != NULL);
	if (CFStringGetCString(serviceNameRef, servicename, namelen, kCFStringEncodingMacRoman))
	{		
		servicename[namelen]='\0';
		for(int i=0;i<gNumLocalGames;i++)
			if(_stricmp(servicename,gLocalGames[i].name)==0)
			{
				MemoryMove(gLocalGames+i,gLocalGames+i+1,(gNumLocalGames-i-1)*sizeof(tGameListEntry));
				gNumLocalGames--;
				gLocalGameListUpdate++;
				return;
			}
	}
}


static void
MyBrowserCallback(CFNetServiceBrowserRef browser, CFOptionFlags flags, CFTypeRef domainOrService, CFStreamError* error, void* info)
{  
    if (flags & kCFNetServiceFlagIsDomain)
        return;

    /* You should do as little work as possible while in the Browser Callback because the mDNSResponder will timeout
    if you don't extract items out of the Mach message queue fast enough.  If a timeout occurs, you will see
    a message in the Console that looks like... "mDNSResponder[260]: 10531: Browser(_afpovertcp._tcp.local.)
    stopped accepting Mach messages (browse)" and from that point, your browsing will no longer return any results. */

    if (flags & kCFNetServiceFlagRemove)
    {
        MyRemoveService((CFNetServiceRef)domainOrService, flags);
    }
    else
    {
        MyAddService((CFNetServiceRef)domainOrService, flags);
    }

    return;
}

static OSStatus
MyStartBrowsingForServices(CFStringRef type, CFStringRef domain)
{
    CFNetServiceClientContext clientContext = { 0, NULL, NULL, NULL, NULL };
    CFStreamError error;
    Boolean result;
    OSStatus		err = noErr;

    assert(type != NULL);
    assert(domain != NULL);

    gServiceBrowserRef = CFNetServiceBrowserCreate(kCFAllocatorDefault, MyBrowserCallback, &clientContext);
    if (gServiceBrowserRef == NULL)
    {
    	err = memFullErr;
    
    }
    else
    {
	    CFNetServiceBrowserScheduleWithRunLoop(gServiceBrowserRef, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
	    
	    result = CFNetServiceBrowserSearchForServices(gServiceBrowserRef, domain, type, &error);
	    
		if (result == FALSE)
		{
	        // Something went wrong so lets clean up.
	        CFNetServiceBrowserUnscheduleFromRunLoop(gServiceBrowserRef, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
	        CFRelease(gServiceBrowserRef);
	        gServiceBrowserRef = NULL;

	        PrintConsoleString("CFNetServiceBrowserSearchForServices returned (domain = %d, error = %ld)\n", error.domain, error.error);
			err = error.error;
		}
		else
			// indicate that the browser task is active
			gBrowserTaskActive = TRUE;
	}

	return err;
}

static void
MyStopBrowsingForServices()
{
	CFStreamError streamerror;
    assert(gServiceBrowserRef != NULL);

	CFNetServiceBrowserStopSearch(gServiceBrowserRef, &streamerror);
	CFNetServiceBrowserUnscheduleFromRunLoop(gServiceBrowserRef, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    CFNetServiceBrowserInvalidate(gServiceBrowserRef);
    CFRelease(gServiceBrowserRef);
    gServiceBrowserRef = NULL;
    
    assert(gServiceDictionary != NULL);
    CFDictionaryRemoveAllValues(gServiceDictionary);

    return;
}

void LocalTrackerStartAdvertising(char *gameName)
{
	MyRegisterService(CFStringCreateWithCString(NULL,gameName,kCFStringEncodingMacRoman),kServiceType,kMyDefaultDomain,33333,NULL);
}

void LocalTrackerStopAdvertising()
{
	MyCancelRegistration();
}

void LocalTrackerSearchGamesInit()
{
//HandleError()
	MyStartBrowsingForServices(kServiceType,kMyDefaultDomain);
}

void LocalTrackerSearchGamesDone()
{
	MyStopBrowsingForServices();
}
