//
//  YSRACScheduler.m
//  ReactiveObjC
//
//  Created by Josh Abernathy on 4/16/12.
//  Copyright (c) 2012 GitHub, Inc. All rights reserved.
//

#import "YSRACScheduler.h"
#import "YSRACCompoundDisposable.h"
#import "YSRACDisposable.h"
#import "YSRACImmediateScheduler.h"
#import "YSRACScheduler+Private.h"
#import "YSRACSubscriptionScheduler.h"
#import "YSRACTargetQueueScheduler.h"

// The key for the thread-specific current scheduler.
NSString * const YSRACSchedulerCurrentSchedulerKey = @"YSRACSchedulerCurrentSchedulerKey";

@interface YSRACScheduler ()
@property (nonatomic, readonly, copy) NSString *name;
@end

@implementation YSRACScheduler

#pragma mark NSObject

- (NSString *)description {
	return [NSString stringWithFormat:@"<%@: %p> %@", self.class, self, self.name];
}

#pragma mark Initializers

- (instancetype)initWithName:(NSString *)name {
	self = [super init];

	if (name == nil) {
		_name = [NSString stringWithFormat:@"org.reactivecocoa.ReactiveObjC.%@.anonymousScheduler", self.class];
	} else {
		_name = [name copy];
	}

	return self;
}

#pragma mark Schedulers

+ (YSRACScheduler *)immediateScheduler {
	static dispatch_once_t onceToken;
	static YSRACScheduler *immediateScheduler;
	dispatch_once(&onceToken, ^{
		immediateScheduler = [[YSRACImmediateScheduler alloc] init];
	});
	
	return immediateScheduler;
}

+ (YSRACScheduler *)mainThreadScheduler {
	static dispatch_once_t onceToken;
	static YSRACScheduler *mainThreadScheduler;
	dispatch_once(&onceToken, ^{
		mainThreadScheduler = [[YSRACTargetQueueScheduler alloc] initWithName:@"org.reactivecocoa.ReactiveObjC.YSRACScheduler.mainThreadScheduler" targetQueue:dispatch_get_main_queue()];
	});
	
	return mainThreadScheduler;
}

+ (YSRACScheduler *)schedulerWithPriority:(YSRACSchedulerPriority)priority name:(NSString *)name {
	return [[YSRACTargetQueueScheduler alloc] initWithName:name targetQueue:dispatch_get_global_queue(priority, 0)];
}

+ (YSRACScheduler *)schedulerWithPriority:(YSRACSchedulerPriority)priority {
	return [self schedulerWithPriority:priority name:@"org.reactivecocoa.ReactiveObjC.YSRACScheduler.backgroundScheduler"];
}

+ (YSRACScheduler *)scheduler {
	return [self schedulerWithPriority:YSRACSchedulerPriorityDefault];
}

+ (YSRACScheduler *)subscriptionScheduler {
	static dispatch_once_t onceToken;
	static YSRACScheduler *subscriptionScheduler;
	dispatch_once(&onceToken, ^{
		subscriptionScheduler = [[YSRACSubscriptionScheduler alloc] init];
	});

	return subscriptionScheduler;
}

+ (BOOL)isOnMainThread {
	return [NSOperationQueue.currentQueue isEqual:NSOperationQueue.mainQueue] || [NSThread isMainThread];
}

+ (YSRACScheduler *)currentScheduler {
	YSRACScheduler *scheduler = NSThread.currentThread.threadDictionary[YSRACSchedulerCurrentSchedulerKey];
	if (scheduler != nil) return scheduler;
	if ([self.class isOnMainThread]) return YSRACScheduler.mainThreadScheduler;

	return nil;
}

#pragma mark Scheduling

- (YSRACDisposable *)schedule:(void (^)(void))block {
	NSCAssert(NO, @"%@ must be implemented by subclasses.", NSStringFromSelector(_cmd));
	return nil;
}

- (YSRACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block {
	NSCAssert(NO, @"%@ must be implemented by subclasses.", NSStringFromSelector(_cmd));
	return nil;
}

- (YSRACDisposable *)afterDelay:(NSTimeInterval)delay schedule:(void (^)(void))block {
	return [self after:[NSDate dateWithTimeIntervalSinceNow:delay] schedule:block];
}

- (YSRACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block {
	NSCAssert(NO, @"%@ must be implemented by subclasses.", NSStringFromSelector(_cmd));
	return nil;
}

- (YSRACDisposable *)scheduleRecursiveBlock:(YSRACSchedulerRecursiveBlock)recursiveBlock {
	YSRACCompoundDisposable *disposable = [YSRACCompoundDisposable compoundDisposable];

	[self scheduleRecursiveBlock:[recursiveBlock copy] addingToDisposable:disposable];
	return disposable;
}

- (void)scheduleRecursiveBlock:(YSRACSchedulerRecursiveBlock)recursiveBlock addingToDisposable:(YSRACCompoundDisposable *)disposable {
	@autoreleasepool {
		YSRACCompoundDisposable *selfDisposable = [YSRACCompoundDisposable compoundDisposable];
		[disposable addDisposable:selfDisposable];

		__weak YSRACDisposable *weakSelfDisposable = selfDisposable;

		YSRACDisposable *schedulingDisposable = [self schedule:^{
			@autoreleasepool {
				// At this point, we've been invoked, so our disposable is now useless.
				[disposable removeDisposable:weakSelfDisposable];
			}

			if (disposable.disposed) return;

			void (^reallyReschedule)(void) = ^{
				if (disposable.disposed) return;
				[self scheduleRecursiveBlock:recursiveBlock addingToDisposable:disposable];
			};

			// Protects the variables below.
			//
			// This doesn't actually need to be __block qualified, but Clang
			// complains otherwise. :C
			__block NSLock *lock = [[NSLock alloc] init];
			lock.name = [NSString stringWithFormat:@"%@ %s", self, sel_getName(_cmd)];

			__block NSUInteger rescheduleCount = 0;

			// Set to YES once synchronous execution has finished. Further
			// rescheduling should occur immediately (rather than being
			// flattened).
			__block BOOL rescheduleImmediately = NO;

			@autoreleasepool {
				recursiveBlock(^{
					[lock lock];
					BOOL immediate = rescheduleImmediately;
					if (!immediate) ++rescheduleCount;
					[lock unlock];

					if (immediate) reallyReschedule();
				});
			}

			[lock lock];
			NSUInteger synchronousCount = rescheduleCount;
			rescheduleImmediately = YES;
			[lock unlock];

			for (NSUInteger i = 0; i < synchronousCount; i++) {
				reallyReschedule();
			}
		}];

		[selfDisposable addDisposable:schedulingDisposable];
	}
}

- (void)performAsCurrentScheduler:(void (^)(void))block {
	NSCParameterAssert(block != NULL);

	// If we're using a concurrent queue, we could end up in here concurrently,
	// in which case we *don't* want to clear the current scheduler immediately
	// after our block is done executing, but only *after* all our concurrent
	// invocations are done.

	YSRACScheduler *previousScheduler = YSRACScheduler.currentScheduler;
	NSThread.currentThread.threadDictionary[YSRACSchedulerCurrentSchedulerKey] = self;

	@autoreleasepool {
		block();
	}

	if (previousScheduler != nil) {
		NSThread.currentThread.threadDictionary[YSRACSchedulerCurrentSchedulerKey] = previousScheduler;
	} else {
		[NSThread.currentThread.threadDictionary removeObjectForKey:YSRACSchedulerCurrentSchedulerKey];
	}
}

@end
