Merge pull request #171 from sgl0v/nsnotificationqueue

Initial implementation of the NSNotificationQueue
diff --git a/Foundation.xcodeproj/project.pbxproj b/Foundation.xcodeproj/project.pbxproj
index 4bb9473..3c6388c 100755
--- a/Foundation.xcodeproj/project.pbxproj
+++ b/Foundation.xcodeproj/project.pbxproj
@@ -204,6 +204,7 @@
 		5BF7AEC01BCD51F9008F214A /* NSUUID.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F4B1BCC5DCB00ED97BB /* NSUUID.swift */; };
 		5BF7AEC11BCD51F9008F214A /* NSValue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BDC3F4C1BCC5DCB00ED97BB /* NSValue.swift */; };
 		5E5835F41C20C9B500C81317 /* TestNSThread.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5E5835F31C20C9B500C81317 /* TestNSThread.swift */; };
+		5EF673AC1C28B527006212A3 /* TestNSNotificationQueue.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5EF673AB1C28B527006212A3 /* TestNSNotificationQueue.swift */; };
 		612952F91C1B235900BE0FD9 /* TestNSNull.swift in Sources */ = {isa = PBXBuildFile; fileRef = 612952F81C1B235900BE0FD9 /* TestNSNull.swift */; };
 		61A395FA1C2484490029B337 /* TestNSLocale.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61A395F91C2484490029B337 /* TestNSLocale.swift */; };
 		61D6C9EF1C1DFE9500DEF583 /* TestNSTimer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 61D6C9EE1C1DFE9500DEF583 /* TestNSTimer.swift */; };
@@ -559,6 +560,7 @@
 		5BF7AEC21BCD568D008F214A /* ForSwiftFoundationOnly.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = ForSwiftFoundationOnly.h; sourceTree = "<group>"; };
 		5E5835F31C20C9B500C81317 /* TestNSThread.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSThread.swift; sourceTree = "<group>"; };
 		5EB6A15C1C188FC40037DCB8 /* TestNSJSONSerialization.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSJSONSerialization.swift; sourceTree = "<group>"; };
+		5EF673AB1C28B527006212A3 /* TestNSNotificationQueue.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSNotificationQueue.swift; sourceTree = "<group>"; };
 		612952F81C1B235900BE0FD9 /* TestNSNull.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSNull.swift; sourceTree = "<group>"; };
 		61A395F91C2484490029B337 /* TestNSLocale.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSLocale.swift; sourceTree = "<group>"; };
 		61D6C9EE1C1DFE9500DEF583 /* TestNSTimer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = TestNSTimer.swift; sourceTree = "<group>"; };
@@ -1085,6 +1087,7 @@
 				5EB6A15C1C188FC40037DCB8 /* TestNSJSONSerialization.swift */,
 				61A395F91C2484490029B337 /* TestNSLocale.swift */,
 				61F8AE7C1C180FC600FB62F0 /* TestNSNotificationCenter.swift */,
+				5EF673AB1C28B527006212A3 /* TestNSNotificationQueue.swift */,
 				EA66F63F1BF1619600136161 /* TestNSNumber.swift */,
 				4DC1D07F1C12EEEF00B5948A /* TestNSPipe.swift */,
 				400E22641C1A4E58007C5933 /* TestNSProcessInfo.swift */,
@@ -1828,6 +1831,7 @@
 				84BA558E1C16F90900F48C54 /* TestNSTimeZone.swift in Sources */,
 				52829AD71C160D64003BC4EF /* TestNSCalendar.swift in Sources */,
 				45BBD92B1C30E9C200540DDB /* TestNSTask.swift in Sources */,
+				5EF673AC1C28B527006212A3 /* TestNSNotificationQueue.swift in Sources */,
 				C93559291C12C49F009FD6A9 /* TestNSAffineTransform.swift in Sources */,
 				DCDBB8331C1768AC00313299 /* TestNSData.swift in Sources */,
 				83712C8E1C1684900049AD49 /* TestNSURLRequest.swift in Sources */,
diff --git a/Foundation/NSNotificationQueue.swift b/Foundation/NSNotificationQueue.swift
index 1923c0e..b94f827 100644
--- a/Foundation/NSNotificationQueue.swift
+++ b/Foundation/NSNotificationQueue.swift
@@ -1,3 +1,11 @@
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2014 - 2015 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
 
 public enum NSPostingStyle : UInt {
     
@@ -15,26 +23,105 @@
 }
 
 public class NSNotificationQueue : NSObject {
-    
+
+    internal typealias NotificationQueueList = NSMutableArray
+    internal typealias NSNotificationListEntry = (NSNotification, [String]) // Notification ans list of modes the notification may be posted in.
+    internal typealias NSNotificationList = [NSNotificationListEntry] // The list of notifications to post
+
+    internal let notificationCenter: NSNotificationCenter
+    internal var asapList = NSNotificationList()
+    internal var idleList = NSNotificationList()
+
+    // The NSNotificationQueue instance is associated with current thread.
+    // The _notificationQueueList represents a list of notification queues related to the current thread.
+    private static var _notificationQueueList = NSThreadSpecific<NSMutableArray>()
+    internal static var notificationQueueList: NotificationQueueList {
+        return _notificationQueueList.get() {
+            return NSMutableArray()
+        }
+    }
+
+    // The default notification queue for the current thread.
+    private static var _defaultQueue = NSThreadSpecific<NSNotificationQueue>()
     public class func defaultQueue() -> NSNotificationQueue {
-        NSUnimplemented()
+        return _defaultQueue.get() {
+            return NSNotificationQueue(notificationCenter: NSNotificationCenter.defaultCenter())
+        }
     }
     
     public init(notificationCenter: NSNotificationCenter) {
-        NSUnimplemented()
+        self.notificationCenter = notificationCenter
+        super.init()
+        NSNotificationQueue.registerQueue(self)
     }
-    
+
+    deinit {
+        NSNotificationQueue.unregisterQueue(self)
+    }
+
     public func enqueueNotification(notification: NSNotification, postingStyle: NSPostingStyle) {
-        NSUnimplemented()
+        enqueueNotification(notification, postingStyle: postingStyle, coalesceMask: [.CoalescingOnName, .CoalescingOnSender], forModes: nil)
     }
 
     public func enqueueNotification(notification: NSNotification, postingStyle: NSPostingStyle, coalesceMask: NSNotificationCoalescing, forModes modes: [String]?) {
-        NSUnimplemented()
+        var runloopModes = [NSDefaultRunLoopMode]
+        if let modes = modes  {
+            runloopModes = modes
+        }
+
+        if !coalesceMask.isEmpty {
+            self.dequeueNotificationsMatching(notification, coalesceMask: coalesceMask)
+        }
+
+        switch postingStyle {
+        case .PostNow:
+            let currentMode = NSRunLoop.currentRunLoop().currentMode
+            if currentMode == nil || runloopModes.contains(currentMode!) {
+                self.notificationCenter.postNotification(notification)
+            }
+        case .PostASAP:
+            self.asapList.append((notification, runloopModes))
+            NSUnimplemented() // post at the end of the current notification callout or timer
+        case .PostWhenIdle:
+            self.idleList.append((notification, runloopModes))
+            NSUnimplemented() // wait until the runloop is idle, then post the notification
+        }
     }
     
-    public func dequeueNotificationsMatching(notification: NSNotification, coalesceMask: Int) {
-        NSUnimplemented()
+    public func dequeueNotificationsMatching(notification: NSNotification, coalesceMask: NSNotificationCoalescing) {
+        var predicate: (NSNotificationListEntry) -> Bool
+        switch coalesceMask {
+        case [.CoalescingOnName, .CoalescingOnSender]:
+            predicate = { (entryNotification, _) in
+                return notification.object === entryNotification.object && notification.name == entryNotification.name
+            }
+        case [.CoalescingOnName]:
+            predicate = { (entryNotification, _) in
+                return notification.name == entryNotification.name
+            }
+        case [.CoalescingOnSender]:
+            predicate = { (entryNotification, _) in
+                return notification.object === entryNotification.object
+            }
+        default:
+            return
+        }
+
+        self.asapList = self.asapList.filter(predicate)
+        self.idleList = self.idleList.filter(predicate)
+    }
+
+    // MARK: Private
+
+    private static func registerQueue(notificationQueue: NSNotificationQueue) {
+        self.notificationQueueList.addObject(notificationQueue)
+    }
+
+    private static func unregisterQueue(notificationQueue: NSNotificationQueue) {
+        guard self.notificationQueueList.indexOfObject(notificationQueue) != NSNotFound else {
+            return
+        }
+        self.notificationQueueList.removeObject(notificationQueue)
     }
 
 }
-
diff --git a/Foundation/NSThread.swift b/Foundation/NSThread.swift
index 066c942..ad2661d 100644
--- a/Foundation/NSThread.swift
+++ b/Foundation/NSThread.swift
@@ -20,13 +20,13 @@
     Unmanaged<AnyObject>.fromOpaque(COpaquePointer(ctx)).release()
 }
 
-private var NSThreadSpecificKeySet = false
-private var NSThreadSpecificKeyLock = NSLock()
-private var NSThreadSpecificKey = pthread_key_t()
-
 internal class NSThreadSpecific<T: AnyObject> {
-    
-    private static var key: pthread_key_t {
+
+    private var NSThreadSpecificKeySet = false
+    private var NSThreadSpecificKeyLock = NSLock()
+    private var NSThreadSpecificKey = pthread_key_t()
+
+    private var key: pthread_key_t {
         get {
             NSThreadSpecificKeyLock.lock()
             if !NSThreadSpecificKeySet {
@@ -40,18 +40,18 @@
     }
     
     internal func get(generator: (Void) -> T) -> T {
-        let specific = pthread_getspecific(NSThreadSpecific.key)
+        let specific = pthread_getspecific(self.key)
         if specific != UnsafeMutablePointer<Void>() {
             return Unmanaged<T>.fromOpaque(COpaquePointer(specific)).takeUnretainedValue()
         } else {
             let value = generator()
-            pthread_setspecific(NSThreadSpecific.key, UnsafePointer<Void>(Unmanaged<AnyObject>.passRetained(value).toOpaque()))
+            pthread_setspecific(self.key, UnsafePointer<Void>(Unmanaged<AnyObject>.passRetained(value).toOpaque()))
             return value
         }
     }
     
     internal func set(value: T) {
-        let specific = pthread_getspecific(NSThreadSpecific.key)
+        let specific = pthread_getspecific(self.key)
         var previous: Unmanaged<T>?
         if specific != UnsafeMutablePointer<Void>() {
             previous = Unmanaged<T>.fromOpaque(COpaquePointer(specific))
@@ -61,7 +61,7 @@
                 return
             }
         }
-        pthread_setspecific(NSThreadSpecific.key, UnsafePointer<Void>(Unmanaged<AnyObject>.passRetained(value).toOpaque()))
+        pthread_setspecific(self.key, UnsafePointer<Void>(Unmanaged<AnyObject>.passRetained(value).toOpaque()))
         if let prev = previous {
             prev.release()
         }
diff --git a/TestFoundation/TestNSNotificationQueue.swift b/TestFoundation/TestNSNotificationQueue.swift
new file mode 100644
index 0000000..27a1356
--- /dev/null
+++ b/TestFoundation/TestNSNotificationQueue.swift
@@ -0,0 +1,150 @@
+// This source file is part of the Swift.org open source project
+//
+// Copyright (c) 2014 - 2015 Apple Inc. and the Swift project authors
+// Licensed under Apache License v2.0 with Runtime Library Exception
+//
+// See http://swift.org/LICENSE.txt for license information
+// See http://swift.org/CONTRIBUTORS.txt for the list of Swift project authors
+//
+
+#if DEPLOYMENT_RUNTIME_OBJC || os(Linux)
+    import Foundation
+    import XCTest
+#else
+    import SwiftFoundation
+    import SwiftXCTest
+#endif
+
+
+class TestNSNotificationQueue : XCTestCase {
+    var allTests : [(String, () -> ())] {
+        return [
+            ("test_defaultQueue", test_defaultQueue),
+            ("test_postNowToDefaultQueueWithoutCoalescing", test_postNowToDefaultQueueWithoutCoalescing),
+            ("test_postNowToDefaultQueueWithCoalescing", test_postNowToDefaultQueueWithCoalescing),
+            ("test_postNowToCustomQueue", test_postNowToCustomQueue),
+            ("test_postNowForDefaultRunLoopMode", test_postNowForDefaultRunLoopMode),
+            ("test_notificationQueueLifecycle", test_notificationQueueLifecycle),
+        ]
+    }
+
+    func test_defaultQueue() {
+        let defaultQueue1 = NSNotificationQueue.defaultQueue()
+        XCTAssertNotNil(defaultQueue1)
+        let defaultQueue2 = NSNotificationQueue.defaultQueue()
+        XCTAssertEqual(defaultQueue1, defaultQueue2)
+
+        executeInBackgroundThread() {
+            let defaultQueueForBackgroundThread = NSNotificationQueue.defaultQueue()
+            XCTAssertNotNil(defaultQueueForBackgroundThread)
+            XCTAssertEqual(defaultQueueForBackgroundThread, NSNotificationQueue.defaultQueue())
+            XCTAssertNotEqual(defaultQueueForBackgroundThread, defaultQueue1)
+        }
+    }
+
+    func test_postNowToDefaultQueueWithoutCoalescing() {
+        let notificationName = "test_postNowWithoutCoalescing"
+        let dummyObject = NSObject()
+        let notification = NSNotification(name: notificationName, object: dummyObject)
+        var numberOfCalls = 0
+        NSNotificationCenter.defaultCenter().addObserverForName(notificationName, object: dummyObject, queue: nil) { notification in
+            numberOfCalls += 1
+        }
+        let queue = NSNotificationQueue.defaultQueue()
+        queue.enqueueNotification(notification, postingStyle: .PostNow)
+        XCTAssertEqual(numberOfCalls, 1)
+    }
+
+    func test_postNowToDefaultQueueWithCoalescing() {
+        let notificationName = "test_postNowToDefaultQueueWithCoalescingOnName"
+        let dummyObject = NSObject()
+        let notification = NSNotification(name: notificationName, object: dummyObject)
+        var numberOfCalls = 0
+        NSNotificationCenter.defaultCenter().addObserverForName(notificationName, object: dummyObject, queue: nil) { notification in
+            numberOfCalls += 1
+        }
+        let queue = NSNotificationQueue.defaultQueue()
+        queue.enqueueNotification(notification, postingStyle: .PostNow)
+        queue.enqueueNotification(notification, postingStyle: .PostNow)
+        queue.enqueueNotification(notification, postingStyle: .PostNow)
+        // Coalescing doesn't work for the NSPostingStyle.PostNow. That is why we expect 3 calls here
+        XCTAssertEqual(numberOfCalls, 3)
+    }
+
+    func test_postNowToCustomQueue() {
+        let notificationName = "test_postNowToCustomQueue"
+        let dummyObject = NSObject()
+        let notification = NSNotification(name: notificationName, object: dummyObject)
+        var numberOfCalls = 0
+        let notificationCenter = NSNotificationCenter()
+        notificationCenter.addObserverForName(notificationName, object: dummyObject, queue: nil) { notification in
+            numberOfCalls += 1
+        }
+        let notificationQueue = NSNotificationQueue(notificationCenter: notificationCenter)
+        notificationQueue.enqueueNotification(notification, postingStyle: .PostNow)
+        XCTAssertEqual(numberOfCalls, 1)
+    }
+
+    func test_postNowForDefaultRunLoopMode() {
+        let notificationName = "test_postNowToDefaultQueueWithCoalescingOnName"
+        let dummyObject = NSObject()
+        let notification = NSNotification(name: notificationName, object: dummyObject)
+        var numberOfCalls = 0
+        NSNotificationCenter.defaultCenter().addObserverForName(notificationName, object: dummyObject, queue: nil) { notification in
+            numberOfCalls += 1
+        }
+        let queue = NSNotificationQueue.defaultQueue()
+
+        let runLoop = NSRunLoop.currentRunLoop()
+        let endDate = NSDate(timeInterval: NSTimeInterval(0.05), sinceDate: NSDate())
+
+        let dummyTimer = NSTimer.scheduledTimer(0.01, repeats: false) { _ in
+            guard let runLoopMode = runLoop.currentMode else {
+                return
+            }
+
+            // post 2 notifications for the NSDefaultRunLoopMode mode
+            queue.enqueueNotification(notification, postingStyle: .PostNow, coalesceMask: [], forModes: [runLoopMode])
+            queue.enqueueNotification(notification, postingStyle: .PostNow)
+            // here we post notification for the NSRunLoopCommonModes. It shouldn't have any affect, because the timer is scheduled in NSDefaultRunLoopMode.
+            // The notification queue will only post the notification to its notification center if the run loop is in one of the modes provided in the array.
+            queue.enqueueNotification(notification, postingStyle: .PostNow, coalesceMask: [], forModes: [NSRunLoopCommonModes])
+        }
+        runLoop.addTimer(dummyTimer, forMode: NSDefaultRunLoopMode)
+        runLoop.runMode(NSDefaultRunLoopMode, beforeDate: endDate)
+        XCTAssertEqual(numberOfCalls, 2)
+    }
+
+    func test_notificationQueueLifecycle() {
+        // check that notificationqueue is associated with current thread. when the thread is destroyed, the queue should be deallocated as well
+        weak var notificationQueue: NSNotificationQueue?
+
+        self.executeInBackgroundThread() {
+            notificationQueue = NSNotificationQueue(notificationCenter: NSNotificationCenter())
+            XCTAssertNotNil(notificationQueue)
+        }
+
+        XCTAssertNil(notificationQueue)
+    }
+
+    // MARK: Private
+
+    private func executeInBackgroundThread(operation: () -> ()) {
+        var isFinished = false
+        let bgThread = NSThread() {
+            operation()
+            isFinished = true
+        }
+        bgThread.start()
+
+        self.waitForExpectation({ isFinished }, withTimeout: 0.2)
+    }
+
+    private func waitForExpectation(expectation: () -> Bool, withTimeout timeout: NSTimeInterval) {
+        let timeoutDate = NSDate(timeIntervalSinceNow: timeout)
+        while !expectation() && timeoutDate.timeIntervalSinceNow > 0.0 {
+            NSRunLoop.currentRunLoop().runUntilDate(NSDate(timeIntervalSinceNow: 0.01))
+        }
+    }
+
+}
diff --git a/TestFoundation/main.swift b/TestFoundation/main.swift
index 82d42c9..206ace5 100644
--- a/TestFoundation/main.swift
+++ b/TestFoundation/main.swift
@@ -39,6 +39,7 @@
     TestNSLocale(),
     TestNSNotificationCenter(),
     TestNSNull(),
+    TestNSNotificationQueue(),
     TestNSNumber(),
     TestNSNumberFormatter(),
     TestNSPipe(),