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

#import "RACTestScheduler.h"
#import <ReactiveObjC/EXTScope.h>
#import "RACCompoundDisposable.h"
#import "RACDisposable.h"
#import "RACScheduler+Private.h"

@interface RACTestSchedulerAction : 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) RACDisposable *disposable;

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

@end

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

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

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

@interface RACTestScheduler ()

// All of the RACTestSchedulerActions 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 RACTestScheduler

#pragma mark Lifecycle

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

	CFBinaryHeapCallBacks callbacks = (CFBinaryHeapCallBacks){
		.version = 0,
		.retain = &RACRetainScheduledAction,
		.release = &RACReleaseScheduledAction,
		.copyDescription = &CFCopyDescription,
		.compare = &RACCompareScheduledActions
	};

	_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;

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

			if (action.disposable.disposed) continue;

			RACScheduler *previousScheduler = RACScheduler.currentScheduler;
			NSThread.currentThread.threadDictionary[RACSchedulerCurrentSchedulerKey] = self;

			action.block();

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

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

#pragma mark RACScheduler

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

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

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

		return action.disposable;
	}
}

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

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

		return action.disposable;
	}
}

- (RACDisposable *)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);

	RACCompoundDisposable *compoundDisposable = [RACCompoundDisposable compoundDisposable];

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

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

			[compoundDisposable removeDisposable:thisDisposable];

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

			block();
		};

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

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

	return compoundDisposable;
}

@end

@implementation RACTestSchedulerAction

#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 = [[RACDisposable alloc] init];

	return self;
}

#pragma mark NSObject

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

@end
