/*
 * OpenClonk, http://www.openclonk.org
 *
 * Copyright (c) 2009-2016, The OpenClonk Team and contributors
 *
 * Distributed under the terms of the ISC license; see accompanying file
 * "COPYING" for details.
 *
 * "Clonk" is a registered trademark of Matthes Bender, used with permission.
 * See accompanying file "TRADEMARK" for details.
 *
 * To redistribute this file separately, substitute the full license texts
 * for the above references.
 */

#include "C4Include.h"
#include "platform/StdScheduler.h"
#import <Cocoa/Cocoa.h>

using namespace std;

@class SCHAdditions;

@interface SCHAddition : NSObject
{
@protected
	__weak SCHAdditions* schedulerAdditions;
	StdSchedulerProc* proc;
}
- (id) initWithProc:(StdSchedulerProc*)_proc;
- (void) registerAt:(SCHAdditions*) _additions;
- (void) unregisterFrom:(SCHAdditions*) _additions;
- (bool) shouldExecuteProc;
- (void) changed;
@end

@interface SCHNotify : SCHAddition
{
	list<pair<CFRunLoopSourceRef, CFSocketRef>> socketSources;
}
- (void) registerAt:(SCHAdditions*) _additions;
- (void) unregisterFrom:(SCHAdditions*) _additions;
@end

@interface SCHTimer : SCHAddition
{
@private
	NSTimer* timer;
}
- (void) registerAt:(SCHAdditions*) _additions;
- (void) unregisterFrom:(SCHAdditions*) _additions;
@end

@interface SCHAdditions : NSObject
{
	NSMutableDictionary* procAdditions;
}
+ (SCHAdditions*) requestAdditionsForScheduler:(StdScheduler*) scheduler;
- (id) initWithScheduler:(StdScheduler*) scheduler;
- (SCHAddition*) additionForProc:(StdSchedulerProc*) proc;
- (SCHAddition*) assignAdditionForProc:(StdSchedulerProc*) proc;
- (void) changed:(SCHAddition*)addition;
- (BOOL) removeAdditionForProc:(StdSchedulerProc*) proc;
- (void) start;
@property(readonly) __weak NSRunLoop* runLoop;
@property(readonly) StdScheduler* scheduler;
@end

@implementation SCHAdditions

static NSMutableDictionary* additionsDictionary;

- (int) numberOfAdditions { return [additionsDictionary count]; }

- (id) initWithScheduler:(StdScheduler*) scheduler
{
	if (self = [super init])
	{
		_scheduler = scheduler;
		procAdditions = [NSMutableDictionary new];
		return self;
	} else
		return nil;
}

- (SCHAddition*) additionForProc:(StdSchedulerProc*) proc
{
	return [procAdditions objectForKey:[NSNumber valueWithPointer:proc]];
}

- (BOOL) removeAdditionForProc:(StdSchedulerProc*) proc
{
	auto key = [NSNumber valueWithPointer:proc];
	SCHAddition* x = [procAdditions objectForKey:key];
	if (x)
	{
		[x unregisterFrom:self];
		[procAdditions removeObjectForKey:key];
		return YES;
	}
	else
		return NO;
}

- (SCHAddition*) assignAdditionForProc:(StdSchedulerProc*) proc
{
	auto timerInterval = proc->TimerInterval();
	auto addition =
		timerInterval ? [[SCHTimer alloc] initWithProc:proc] :
		proc->IsNotify() ? [[SCHNotify alloc] initWithProc:proc] :
		nullptr;
	if (addition)
	{
		[procAdditions setObject:addition forKey:[NSNumber valueWithPointer:proc]];
		if (_runLoop)
			[addition registerAt:self];
		return addition;
	} else
		return nullptr;
}

- (void) start
{
	auto current = [NSRunLoop currentRunLoop];
	if (current != [NSRunLoop mainRunLoop])
		return; // oh well
	_runLoop = current;
	[procAdditions enumerateKeysAndObjectsUsingBlock:
		^void (id key, SCHAddition* obj, BOOL* stop) { [obj registerAt:self]; }];
}

- (void) changed:(SCHAddition*)addition
{
	[addition unregisterFrom:self];
	if (_runLoop)
		[addition registerAt:self];
}

+ (void) removeAdditions:(SCHAdditions*)additions
{
	auto key = [NSNumber valueWithPointer:additions.scheduler];
	@synchronized (additionsDictionary)
	{
		[additionsDictionary removeObjectForKey:key];
	}
}

+ (SCHAdditions*) requestAdditionsForScheduler:(StdScheduler *)scheduler
{
	static dispatch_once_t onceToken;
	dispatch_once(&onceToken,
		^{ additionsDictionary = [NSMutableDictionary new]; });
	auto key = [NSNumber valueWithPointer:scheduler];
	@synchronized (additionsDictionary)
	{
		SCHAdditions* additions = [additionsDictionary objectForKey:key];
		if (!additions)
		{
			additions = [[SCHAdditions alloc] initWithScheduler:scheduler];
			[additionsDictionary setObject:additions forKey:key];
		}
		return additions;
	}
}

@end

@implementation SCHAddition
- (id) initWithProc:(StdSchedulerProc *) _proc
{
	if (self = [super init])
	{
		proc = _proc;
		return self;
	} else
		return nil;
}
- (bool) shouldExecuteProc
{
	if (!proc)
		return false;
	auto s = schedulerAdditions;
	return s && !s.scheduler->IsInManualLoop();
}
- (void) registerAt:(SCHAdditions*) _additions
{
	schedulerAdditions = _additions;
}
- (void) unregisterFrom:(SCHAdditions*) _additions
{
	schedulerAdditions = nil;
}
- (void) changed
{
	[schedulerAdditions changed:self];
}
@end

@implementation SCHTimer
- (id) initWithProc:(StdSchedulerProc *) _proc
{
	if (self = [super init])
	{
		proc = _proc;
		return self;
	} else
		return nil;
}
- (void) run:(id) sender
{
	auto i = timer;
	if (i && [self shouldExecuteProc])
		proc->Execute();
}
- (void) registerAt:(SCHAdditions*) _additions
{
	[super registerAt:_additions];
	auto loop = _additions.runLoop;
	timer = [NSTimer timerWithTimeInterval:proc->TimerInterval()/1000.0 target:self selector:@selector(run:) userInfo:nil repeats:YES];
	[loop addTimer:timer forMode:NSDefaultRunLoopMode];
}
- (void) unregisterFrom:(SCHAdditions*) _additions
{
	[timer invalidate];
	timer = nil;
	[super unregisterFrom:_additions];
}
@end

@implementation SCHNotify
void callback (CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void *data, void *info)
{
	auto notify = (__bridge SCHNotify*)info;
	pollfd p = {.fd=CFSocketGetNative(s)};
	if ([notify shouldExecuteProc])
		notify->proc->Execute(-1, &p);
}
- (void) registerAt:(SCHAdditions*) _additions
{
	[super registerAt:_additions];
	vector<struct pollfd> vecs;
	proc->GetFDs(vecs);
	CFSocketContext ctx =
	{
		.info = (__bridge void*)self,
		.retain = CFRetain,
		.release = CFRelease
	};
	for (auto p : vecs)
	{
		auto socket = CFSocketCreateWithNative(NULL,
			p.fd, kCFSocketReadCallBack,
			callback, &ctx
		);
		auto runLoopSource = CFSocketCreateRunLoopSource(NULL, socket, 0);
		CFRunLoopAddSource([_additions.runLoop getCFRunLoop], runLoopSource, kCFRunLoopDefaultMode);
		socketSources.push_back(make_pair(runLoopSource, socket));
	}
}
- (void) unregisterFrom:(SCHAdditions*) _additions
{
	auto runLoop = [_additions.runLoop getCFRunLoop];
	for (auto r : socketSources)
	{
		CFRunLoopRemoveSource(runLoop, r.first, kCFRunLoopDefaultMode);
		CFSocketDisableCallBacks(r.second, kCFSocketReadCallBack|kCFSocketWriteCallBack);
		CFRunLoopSourceInvalidate(r.first);
		CFRelease(r.second);
		CFRelease(r.first);
	}
	socketSources.clear();
	[super unregisterFrom:_additions];
}
@end

void StdScheduler::StartOnCurrentThread()
{
	[[SCHAdditions requestAdditionsForScheduler:this] start];
}

void StdScheduler::Added(StdSchedulerProc *pProc)
{
	[[SCHAdditions requestAdditionsForScheduler:this] assignAdditionForProc:pProc];
}

void StdScheduler::Removing(StdSchedulerProc *pProc)
{
	auto x = [SCHAdditions requestAdditionsForScheduler:this];
	[x removeAdditionForProc:pProc];
	if ([x numberOfAdditions] == 0)
		[SCHAdditions removeAdditions:x];
}

void StdScheduler::Changed(StdSchedulerProc* pProc)
{
	[[[SCHAdditions requestAdditionsForScheduler:this] additionForProc:pProc] changed];
}
