//
//  YSRACCompoundDisposable.m
//  ReactiveObjC
//
//  Created by Josh Abernathy on 11/30/12.
//  Copyright (c) 2012 GitHub, Inc. All rights reserved.
//

#import "YSRACCompoundDisposable.h"
#import "YSRACCompoundDisposableProvider.h"
#import <pthread/pthread.h>

// The number of child disposables for which space will be reserved directly in
// `YSRACCompoundDisposable`.
//
// This number has been empirically determined to provide a good tradeoff
// between performance, memory usage, and `YSRACCompoundDisposable` instance size
// in a moderately complex GUI application.
//
// Profile any change!
#define YSRACCompoundDisposableInlineCount 2

static CFMutableArrayRef YSRACCreateDisposablesArray(void) {
	// Compare values using only pointer equality.
	CFArrayCallBacks callbacks = kCFTypeArrayCallBacks;
	callbacks.equal = NULL;

	return CFArrayCreateMutable(NULL, 0, &callbacks);
}

@interface YSRACCompoundDisposable () {
	// Used for synchronization.
	pthread_mutex_t _mutex;

	#if YSRACCompoundDisposableInlineCount
	// A fast array to the first N of the receiver's disposables.
	//
	// Once this is full, `_disposables` will be created and used for additional
	// disposables.
	//
	// This array should only be manipulated while _mutex is held.
	YSRACDisposable *_inlineDisposables[YSRACCompoundDisposableInlineCount];
	#endif

	// Contains the receiver's disposables.
	//
	// This array should only be manipulated while _mutex is held. If
	// `_disposed` is YES, this may be NULL.
	CFMutableArrayRef _disposables;

	// Whether the receiver has already been disposed.
	//
	// This ivar should only be accessed while _mutex is held.
	BOOL _disposed;
}

@end

@implementation YSRACCompoundDisposable

#pragma mark Properties

- (BOOL)isDisposed {
	pthread_mutex_lock(&_mutex);
	BOOL disposed = _disposed;
	pthread_mutex_unlock(&_mutex);

	return disposed;
}

#pragma mark Lifecycle

+ (instancetype)compoundDisposable {
	return [[self alloc] initWithDisposables:nil];
}

+ (instancetype)compoundDisposableWithDisposables:(NSArray *)disposables {
	return [[self alloc] initWithDisposables:disposables];
}

- (instancetype)init {
	self = [super init];

	const int result __attribute__((unused)) = pthread_mutex_init(&_mutex, NULL);
	NSCAssert(0 == result, @"Failed to initialize mutex with error %d.", result);

	return self;
}

- (instancetype)initWithDisposables:(NSArray *)otherDisposables {
	self = [self init];

	#if YSRACCompoundDisposableInlineCount
	[otherDisposables enumerateObjectsUsingBlock:^(YSRACDisposable *disposable, NSUInteger index, BOOL *stop) {
		self->_inlineDisposables[index] = disposable;

		// Stop after this iteration if we've reached the end of the inlined
		// array.
		if (index == YSRACCompoundDisposableInlineCount - 1) *stop = YES;
	}];
	#endif

	if (otherDisposables.count > YSRACCompoundDisposableInlineCount) {
		_disposables = YSRACCreateDisposablesArray();

		CFRange range = CFRangeMake(YSRACCompoundDisposableInlineCount, (CFIndex)otherDisposables.count - YSRACCompoundDisposableInlineCount);
		CFArrayAppendArray(_disposables, (__bridge CFArrayRef)otherDisposables, range);
	}

	return self;
}

- (instancetype)initWithBlock:(void (^)(void))block {
	YSRACDisposable *disposable = [YSRACDisposable disposableWithBlock:block];
	return [self initWithDisposables:@[ disposable ]];
}

- (void)dealloc {
	#if YSRACCompoundDisposableInlineCount
	for (unsigned i = 0; i < YSRACCompoundDisposableInlineCount; i++) {
		_inlineDisposables[i] = nil;
	}
	#endif

	if (_disposables != NULL) {
		CFRelease(_disposables);
		_disposables = NULL;
	}

	const int result __attribute__((unused)) = pthread_mutex_destroy(&_mutex);
	NSCAssert(0 == result, @"Failed to destroy mutex with error %d.", result);
}

#pragma mark Addition and Removal

- (void)addDisposable:(YSRACDisposable *)disposable {
	NSCParameterAssert(disposable != self);
	if (disposable == nil || disposable.disposed) return;

	BOOL shouldDispose = NO;

	pthread_mutex_lock(&_mutex);
	{
		if (_disposed) {
			shouldDispose = YES;
		} else {
			#if YSRACCompoundDisposableInlineCount
			for (unsigned i = 0; i < YSRACCompoundDisposableInlineCount; i++) {
				if (_inlineDisposables[i] == nil) {
					_inlineDisposables[i] = disposable;
					goto foundSlot;
				}
			}
			#endif

			if (_disposables == NULL) _disposables = YSRACCreateDisposablesArray();
			CFArrayAppendValue(_disposables, (__bridge void *)disposable);

			if (YSRACCOMPOUNDDISPOSABLE_ADDED_ENABLED()) {
				YSRACCOMPOUNDDISPOSABLE_ADDED(self.description.UTF8String, disposable.description.UTF8String, CFArrayGetCount(_disposables) + YSRACCompoundDisposableInlineCount);
			}

		#if YSRACCompoundDisposableInlineCount
		foundSlot:;
		#endif
		}
	}
	pthread_mutex_unlock(&_mutex);

	// Performed outside of the lock in case the compound disposable is used
	// recursively.
	if (shouldDispose) [disposable dispose];
}

- (void)removeDisposable:(YSRACDisposable *)disposable {
	if (disposable == nil) return;

	pthread_mutex_lock(&_mutex);
	{
		if (!_disposed) {
			#if YSRACCompoundDisposableInlineCount
			for (unsigned i = 0; i < YSRACCompoundDisposableInlineCount; i++) {
				if (_inlineDisposables[i] == disposable) _inlineDisposables[i] = nil;
			}
			#endif

			if (_disposables != NULL) {
				CFIndex count = CFArrayGetCount(_disposables);
				for (CFIndex i = count - 1; i >= 0; i--) {
					const void *item = CFArrayGetValueAtIndex(_disposables, i);
					if (item == (__bridge void *)disposable) {
						CFArrayRemoveValueAtIndex(_disposables, i);
					}
				}

				if (YSRACCOMPOUNDDISPOSABLE_REMOVED_ENABLED()) {
					YSRACCOMPOUNDDISPOSABLE_REMOVED(self.description.UTF8String, disposable.description.UTF8String, CFArrayGetCount(_disposables) + YSRACCompoundDisposableInlineCount);
				}
			}
		}
	}
	pthread_mutex_unlock(&_mutex);
}

#pragma mark YSRACDisposable

static void disposeEach(const void *value, void *context) {
	YSRACDisposable *disposable = (__bridge id)value;
	[disposable dispose];
}

- (void)dispose {
	#if YSRACCompoundDisposableInlineCount
	YSRACDisposable *inlineCopy[YSRACCompoundDisposableInlineCount];
	#endif

	CFArrayRef remainingDisposables = NULL;

	pthread_mutex_lock(&_mutex);
	{
		_disposed = YES;

		#if YSRACCompoundDisposableInlineCount
		for (unsigned i = 0; i < YSRACCompoundDisposableInlineCount; i++) {
			inlineCopy[i] = _inlineDisposables[i];
			_inlineDisposables[i] = nil;
		}
		#endif

		remainingDisposables = _disposables;
		_disposables = NULL;
	}
	pthread_mutex_unlock(&_mutex);

	#if YSRACCompoundDisposableInlineCount
	// Dispose outside of the lock in case the compound disposable is used
	// recursively.
	for (unsigned i = 0; i < YSRACCompoundDisposableInlineCount; i++) {
		[inlineCopy[i] dispose];
	}
	#endif

	if (remainingDisposables == NULL) return;

	CFIndex count = CFArrayGetCount(remainingDisposables);
	CFArrayApplyFunction(remainingDisposables, CFRangeMake(0, count), &disposeEach, NULL);
	CFRelease(remainingDisposables);
}

@end
