//
//  YSRACTestScheduler.m
//  ReactiveObjC
//
//  Created by Justin Spahr-Summers on 2013-07-06.
//  Copyright (c) 2013 GitHub, Inc. All rights reserved.
//

#import "YSRACTestScheduler.h"
#import "YSRACEXTScope.h"
#import "YSRACCompoundDisposable.h"
#import "YSRACDisposable.h"
#import "YSRACScheduler+Private.h"

@interface YSRACTestSchedulerAction : NSObject

// The date at which the action should be executed.
//
// This absolute time will not actually be honored. This date is only used for
// comparison, to determine which block should be run _next_.
@property (nonatomic, copy, readonly) NSDate *date;

// The scheduled block.
@property (nonatomic, copy, readonly) void (^block)(void);

// A disposable for this action.
//
// When disposed, the action should not start executing if it hasn't already.
@property (nonatomic, strong, readonly) YSRACDisposable *disposable;

// Initializes a new scheduler action.
- (instancetype)initWithDate:(NSDate *)date block:(void (^)(void))block;

@end

static CFComparisonResult YSRACCompareScheduledActions(const void *ptr1, const void *ptr2, void *info) {
	YSRACTestSchedulerAction *action1 = (__bridge id)ptr1;
	YSRACTestSchedulerAction *action2 = (__bridge id)ptr2;
	return CFDateCompare((__bridge CFDateRef)action1.date, (__bridge CFDateRef)action2.date, NULL);
}

static const void *YSRACRetainScheduledAction(CFAllocatorRef allocator, const void *ptr) {
	return CFRetain(ptr);
}

static void YSRACReleaseScheduledAction(CFAllocatorRef allocator, const void *ptr) {
	CFRelease(ptr);
}

@interface YSRACTestScheduler ()

// All of the YSRACTestSchedulerActions that have been enqueued and not yet
// executed.
//
// The minimum value in the heap represents the action to execute next.
//
// This property should only be used while synchronized on self.
@property (nonatomic, assign, readonly) CFBinaryHeapRef scheduledActions;

// The number of blocks that have been directly enqueued with -schedule: so
// far.
//
// This is used to ensure unique dates when two blocks are enqueued
// simultaneously.
//
// This property should only be used while synchronized on self.
@property (nonatomic, assign) NSUInteger numberOfDirectlyScheduledBlocks;

@end

@implementation YSRACTestScheduler

#pragma mark Lifecycle

- (instancetype)init {
	self = [super initWithName:@"org.reactivecocoa.ReactiveObjC.YSRACTestScheduler"];

	CFBinaryHeapCallBacks callbacks = (CFBinaryHeapCallBacks){
		.version = 0,
		.retain = &YSRACRetainScheduledAction,
		.release = &YSRACReleaseScheduledAction,
		.copyDescription = &CFCopyDescription,
		.compare = &YSRACCompareScheduledActions
	};

	_scheduledActions = CFBinaryHeapCreate(NULL, 0, &callbacks, NULL);
	return self;
}

- (void)dealloc {
	[self stepAll];

	if (_scheduledActions != NULL) {
		CFBridgingRelease(_scheduledActions);
		_scheduledActions = NULL;
	}
}

#pragma mark Execution

- (void)step {
	[self step:1];
}

- (void)step:(NSUInteger)ticks {
	@synchronized (self) {
		for (NSUInteger i = 0; i < ticks; i++) {
			const void *actionPtr = NULL;
			if (!CFBinaryHeapGetMinimumIfPresent(self.scheduledActions, &actionPtr)) break;

			YSRACTestSchedulerAction *action = (__bridge id)actionPtr;
			CFBinaryHeapRemoveMinimumValue(self.scheduledActions);

			if (action.disposable.disposed) continue;

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

			action.block();

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

- (void)stepAll {
	[self step:NSUIntegerMax];
}

#pragma mark YSRACScheduler

- (YSRACDisposable *)schedule:(void (^)(void))block {
	NSCParameterAssert(block != nil);

	@synchronized (self) {
		NSDate *uniqueDate = [NSDate dateWithTimeIntervalSinceReferenceDate:self.numberOfDirectlyScheduledBlocks];
		self.numberOfDirectlyScheduledBlocks++;

		YSRACTestSchedulerAction *action = [[YSRACTestSchedulerAction alloc] initWithDate:uniqueDate block:block];
		CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action);

		return action.disposable;
	}
}

- (YSRACDisposable *)after:(NSDate *)date schedule:(void (^)(void))block {
	NSCParameterAssert(date != nil);
	NSCParameterAssert(block != nil);

	@synchronized (self) {
		YSRACTestSchedulerAction *action = [[YSRACTestSchedulerAction alloc] initWithDate:date block:block];
		CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action);

		return action.disposable;
	}
}

- (YSRACDisposable *)after:(NSDate *)date repeatingEvery:(NSTimeInterval)interval withLeeway:(NSTimeInterval)leeway schedule:(void (^)(void))block {
	NSCParameterAssert(date != nil);
	NSCParameterAssert(block != nil);
	NSCParameterAssert(interval >= 0);
	NSCParameterAssert(leeway >= 0);

	YSRACCompoundDisposable *compoundDisposable = [YSRACCompoundDisposable compoundDisposable];

	@weakify(self);
	@synchronized (self) {
		__block YSRACDisposable *thisDisposable = nil;

		void (^reschedulingBlock)(void) = ^{
			@strongify(self);

			[compoundDisposable removeDisposable:thisDisposable];

			// Schedule the next interval.
			YSRACDisposable *schedulingDisposable = [self after:[date dateByAddingTimeInterval:interval] repeatingEvery:interval withLeeway:leeway schedule:block];
			[compoundDisposable addDisposable:schedulingDisposable];

			block();
		};

		YSRACTestSchedulerAction *action = [[YSRACTestSchedulerAction alloc] initWithDate:date block:reschedulingBlock];
		CFBinaryHeapAddValue(self.scheduledActions, (__bridge void *)action);

		thisDisposable = action.disposable;
		[compoundDisposable addDisposable:thisDisposable];
	}

	return compoundDisposable;
}

@end

@implementation YSRACTestSchedulerAction

#pragma mark Lifecycle

- (instancetype)initWithDate:(NSDate *)date block:(void (^)(void))block {
	NSCParameterAssert(date != nil);
	NSCParameterAssert(block != nil);

	self = [super init];

	_date = [date copy];
	_block = [block copy];
	_disposable = [[YSRACDisposable alloc] init];

	return self;
}

#pragma mark NSObject

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

@end
