//
//  RACSerialDisposable.m
//  ReactiveCocoa
//
//  Created by Justin Spahr-Summers on 2013-07-22.
//  Copyright (c) 2013 GitHub, Inc. All rights reserved.
//

#import "RACSerialDisposable.h"
#import <libkern/OSAtomic.h>

@interface RACSerialDisposable () {
	// A reference to the receiver's `disposable`. This variable must only be
	// modified atomically.
	//
	// If this is `self`, no `disposable` has been set, but the receiver has not
	// been disposed of yet. `self` is never stored retained.
	//
	// If this is `nil`, the receiver has been disposed.
	//
	// Otherwise, this is a retained reference to the inner disposable and the
	// receiver has not been disposed of yet.
	void * volatile _disposablePtr;
}

@end

@implementation RACSerialDisposable

#pragma mark Properties

- (BOOL)isDisposed {
	return _disposablePtr == nil;
}

- (RACDisposable *)disposable {
	RACDisposable *disposable = (__bridge id)_disposablePtr;
	return (disposable == self ? nil : disposable);
}

- (void)setDisposable:(RACDisposable *)disposable {
	[self swapInDisposable:disposable];
}

#pragma mark Lifecycle

+ (instancetype)serialDisposableWithDisposable:(RACDisposable *)disposable {
	RACSerialDisposable *serialDisposable = [[self alloc] init];
	serialDisposable.disposable = disposable;
	return serialDisposable;
}

- (id)init {
	self = [super init];
	if (self == nil) return nil;

	_disposablePtr = (__bridge void *)self;
	OSMemoryBarrier();

	return self;
}

- (id)initWithBlock:(void (^)(void))block {
	self = [self init];
	if (self == nil) return nil;

	self.disposable = [RACDisposable disposableWithBlock:block];

	return self;
}

- (void)dealloc {
	self.disposable = nil;
}

#pragma mark Inner Disposable

- (RACDisposable *)swapInDisposable:(RACDisposable *)newDisposable {
	void * const selfPtr = (__bridge void *)self;

	// Only retain the new disposable if it's not `self`.
	// Take ownership before attempting the swap so that a subsequent swap
	// receives an owned reference.
	void *newDisposablePtr = selfPtr;
	if (newDisposable != nil) {
		newDisposablePtr = (void *)CFBridgingRetain(newDisposable);
	}

	void *existingDisposablePtr;
	// Keep trying while we're not disposed.
	while ((existingDisposablePtr = _disposablePtr) != NULL) {
		if (!OSAtomicCompareAndSwapPtrBarrier(existingDisposablePtr, newDisposablePtr, &_disposablePtr)) {
			continue;
		}

		// Return nil if _disposablePtr was set to self. Otherwise, release
		// the old value and return it as an object.
		if (existingDisposablePtr == selfPtr) {
			return nil;
		} else {
			return CFBridgingRelease(existingDisposablePtr);
		}
	}

	// At this point, we've found out that we were already disposed.
	[newDisposable dispose];

	// Failed to swap, clean up the ownership we took prior to the swap.
	if (newDisposable != nil) {
		CFRelease(newDisposablePtr);
	}

	return nil;
}

#pragma mark Disposal

- (void)dispose {
	void *existingDisposablePtr;

	while ((existingDisposablePtr = _disposablePtr) != NULL) {
		if (OSAtomicCompareAndSwapPtrBarrier(existingDisposablePtr, NULL, &_disposablePtr)) {
			if (existingDisposablePtr != (__bridge void *)self) {
				RACDisposable *existingDisposable = CFBridgingRelease(existingDisposablePtr);
				[existingDisposable dispose];
			}

			break;
		}
	}
}

@end
