<commit_before>//
// https://github.com/SteveKChiu/CoreDataMonk
//
// Copyright 2015, <NAME> <<EMAIL>>
//
// The MIT License (http://www.opensource.org/licenses/mit-license.php)
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//

import UIKit
import CoreData

//---------------------------------------------------------------------------

private class CollectionViewDataBridge<EntityType: NSManagedObject>
        : NSObject, UICollectionViewDataSource, NSFetchedResultsControllerDelegate {
    weak var provider: CollectionViewDataProvider<EntityType>?
    var pendingActions: [() -> Void] = []
    var updatedIndexPaths: Set<NSIndexPath> = []
    var shouldReloadData = false
    var isFiltering = false
    var semaphore: dispatch_semaphore_t
    
    var collectionView: UICollectionView? {
        return self.provider?.collectionView
    }
    
    init(provider: CollectionViewDataProvider<EntityType>) {
        self.provider = provider
        self.semaphore = dispatch_semaphore_create(1)
    }
    
    @objc func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return self.provider?.numberOfSections() ?? 0
    }

    @objc func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.provider?.numberOfObjectsInSection(section) ?? 0
    }
    
    @objc func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        if let object = self.provider?.objectAtIndexPath(indexPath),
               cell = self.provider?.onGetCell?(object, indexPath) {
            return cell
        }
        return UICollectionViewCell()
    }
    
    @objc func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
        if let view = self.provider?.onGetSupplementary?(kind, indexPath) {
            return view
        }
        return UICollectionReusableView()
    }
    
    @objc func controllerWillChangeContent(controller: NSFetchedResultsController) {
        self.pendingActions.removeAll()
        self.updatedIndexPaths.removeAll()
        self.shouldReloadData = false
        self.isFiltering = self.provider?.objectFilter != nil
    }

    @objc func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
        if self.isFiltering || self.shouldReloadData {
            return
        }
    
        switch type {
        case .Insert:
            if self.updatedIndexPaths.contains(newIndexPath!) {
                self.pendingActions.removeAll()
                self.shouldReloadData = true
            } else {
                self.pendingActions.append() {
                    [weak self] in
                    self?.collectionView?.insertItemsAtIndexPaths([ newIndexPath! ])
                }
            }
            
        case .Delete:
            self.pendingActions.append() {
                [weak self] in
                self?.collectionView?.deleteItemsAtIndexPaths([ indexPath! ])
            }
            self.updatedIndexPaths.remove(indexPath!)
            
        case .Move:
            self.pendingActions.append() {
                [weak self] in
                self?.collectionView?.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
            }
            self.updatedIndexPaths.remove(indexPath!)
            
        case .Update:
            self.updatedIndexPaths.insert(indexPath!)
        }
    }
    
    @objc func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
        if self.isFiltering || self.shouldReloadData {
            return
        }

        switch type {
        case .Insert:
            self.pendingActions.append() {
                [weak self] in
                self?.collectionView?.insertSections(NSIndexSet(index: sectionIndex))
                self?.collectionView?.collectionViewLayout.invalidateLayout()
            }
            
        case .Delete:
            self.pendingActions.append() {
                [weak self] in
                self?.collectionView?.deleteSections(NSIndexSet(index: sectionIndex))
                self?.collectionView?.collectionViewLayout.invalidateLayout()
            }
            
        default:
            break
        }
    }
    
    @objc func controllerDidChangeContent(controller: NSFetchedResultsController) {
        if self.isFiltering {
            self.provider?.filter()
            self.provider?.onDataChanged?()
            return
        }

        if self.shouldReloadData || self.collectionView?.window == nil {
            self.pendingActions.removeAll()
            self.updatedIndexPaths.removeAll()
            self.collectionView?.reloadData()
            return
        }

        let semaphore = self.semaphore
        dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC)))
        self.collectionView?.performBatchUpdates({
            [weak self] in
            if let actions = self?.pendingActions where !actions.isEmpty {
                self?.pendingActions.removeAll()
                for action in actions {
                    action()
                }
            }<commit_msg>Make sure collection view exists before batch update<commit_after>//
// https://github.com/SteveKChiu/CoreDataMonk
//
// Copyright 2015, <NAME> <<EMAIL>>
//
// The MIT License (http://www.opensource.org/licenses/mit-license.php)
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
//

import UIKit
import CoreData

//---------------------------------------------------------------------------

private class CollectionViewDataBridge<EntityType: NSManagedObject>
        : NSObject, UICollectionViewDataSource, NSFetchedResultsControllerDelegate {
    weak var provider: CollectionViewDataProvider<EntityType>?
    var pendingActions: [() -> Void] = []
    var updatedIndexPaths: Set<NSIndexPath> = []
    var shouldReloadData = false
    var isFiltering = false
    var semaphore: dispatch_semaphore_t
    
    var collectionView: UICollectionView? {
        return self.provider?.collectionView
    }
    
    init(provider: CollectionViewDataProvider<EntityType>) {
        self.provider = provider
        self.semaphore = dispatch_semaphore_create(1)
    }
    
    @objc func numberOfSectionsInCollectionView(collectionView: UICollectionView) -> Int {
        return self.provider?.numberOfSections() ?? 0
    }

    @objc func collectionView(collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        return self.provider?.numberOfObjectsInSection(section) ?? 0
    }
    
    @objc func collectionView(collectionView: UICollectionView, cellForItemAtIndexPath indexPath: NSIndexPath) -> UICollectionViewCell {
        if let object = self.provider?.objectAtIndexPath(indexPath),
               cell = self.provider?.onGetCell?(object, indexPath) {
            return cell
        }
        return UICollectionViewCell()
    }
    
    @objc func collectionView(collectionView: UICollectionView, viewForSupplementaryElementOfKind kind: String, atIndexPath indexPath: NSIndexPath) -> UICollectionReusableView {
        if let view = self.provider?.onGetSupplementary?(kind, indexPath) {
            return view
        }
        return UICollectionReusableView()
    }
    
    @objc func controllerWillChangeContent(controller: NSFetchedResultsController) {
        self.pendingActions.removeAll()
        self.updatedIndexPaths.removeAll()
        self.shouldReloadData = false
        self.isFiltering = self.provider?.objectFilter != nil
    }

    @objc func controller(controller: NSFetchedResultsController, didChangeObject anObject: AnyObject, atIndexPath indexPath: NSIndexPath?, forChangeType type: NSFetchedResultsChangeType, newIndexPath: NSIndexPath?) {
        if self.isFiltering || self.shouldReloadData {
            return
        }
    
        switch type {
        case .Insert:
            if self.updatedIndexPaths.contains(newIndexPath!) {
                self.pendingActions.removeAll()
                self.shouldReloadData = true
            } else {
                self.pendingActions.append() {
                    [weak self] in
                    self?.collectionView?.insertItemsAtIndexPaths([ newIndexPath! ])
                }
            }
            
        case .Delete:
            self.pendingActions.append() {
                [weak self] in
                self?.collectionView?.deleteItemsAtIndexPaths([ indexPath! ])
            }
            self.updatedIndexPaths.remove(indexPath!)
            
        case .Move:
            self.pendingActions.append() {
                [weak self] in
                self?.collectionView?.moveItemAtIndexPath(indexPath!, toIndexPath: newIndexPath!)
            }
            self.updatedIndexPaths.remove(indexPath!)
            
        case .Update:
            self.updatedIndexPaths.insert(indexPath!)
        }
    }
    
    @objc func controller(controller: NSFetchedResultsController, didChangeSection sectionInfo: NSFetchedResultsSectionInfo, atIndex sectionIndex: Int, forChangeType type: NSFetchedResultsChangeType) {
        if self.isFiltering || self.shouldReloadData {
            return
        }

        switch type {
        case .Insert:
            self.pendingActions.append() {
                [weak self] in
                self?.collectionView?.insertSections(NSIndexSet(index: sectionIndex))
                self?.collectionView?.collectionViewLayout.invalidateLayout()
            }
            
        case .Delete:
            self.pendingActions.append() {
                [weak self] in
                self?.collectionView?.deleteSections(NSIndexSet(index: sectionIndex))
                self?.collectionView?.collectionViewLayout.invalidateLayout()
            }
            
        default:
            break
        }
    }
    
    @objc func controllerDidChangeContent(controller: NSFetchedResultsController) {
        if self.isFiltering {
            self.provider?.filter()
            self.provider?.onDataChanged?()
            return
        }

        if self.shouldReloadData || self.collectionView?.window == nil {
            self.pendingActions.removeAll()
            self.updatedIndexPaths.removeAll()
            self.collectionView?.reloadData()
            return
        }

        guard let collectionView = self.collectionView else {
            return
        }

        // make sure batch update animation is not overlapped
        let semaphore = self.semaphore
        dispatch_semaphore_wait(semaphore, dispatch_time(DISPATCH_TIME_NOW, Int64(NSEC_PER_SEC)))

        collectionView.performBatchUpdates({
            [weak self] in
            if let actions = self?.pendingActions where !actions.isEmpty {
                self?.pendingActions.removeAll()
                for action in actions {
                    action()
                }
            }
{'fraction_non_alphanumeric': 0.09133173365326934, 'fraction_numerical': 0.0013497300539892022, 'mean_word_length': 2.3077876984126986, 'pattern_counts': {'":': 0, '<': 17, '<?xml version=': 0, '>': 27, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '7311129', 'n_tokens_mistral': 3377, 'n_tokens_neox': 3274, 'n_words': 855}