/*
 modified
 */

/*
    Copyright (C) 2016 Apple Inc. All Rights Reserved.
    See LICENSE.txt for this sample’s licensing information
    
    Abstract:
    `ThumbnailCache` manages loading thumbnails on background queues and keeping track of which thumbnails are up to date. It also stores thumbnails in a cache for quick access at a later time.
 */

/* LICENSE.txt
 Sample code project: ShapeEdit: Building a Simple iCloud Document App
 Version: 1.3

 IMPORTANT:  This Apple software is supplied to you by Apple
 Inc. ("Apple") in consideration of your agreement to the following
 terms, and your use, installation, modification or redistribution of
 this Apple software constitutes acceptance of these terms.  If you do
 not agree with these terms, please do not use, install, modify or
 redistribute this Apple software.

 In consideration of your agreement to abide by the following terms, and
 subject to these terms, Apple grants you a personal, non-exclusive
 license, under Apple's copyrights in this original Apple software (the
 "Apple Software"), to use, reproduce, modify and redistribute the Apple
 Software, with or without modifications, in source and/or binary forms;
 provided that if you redistribute the Apple Software in its entirety and
 without modifications, you must retain this notice and the following
 text and disclaimers in all such redistributions of the Apple Software.
 Neither the name, trademarks, service marks or logos of Apple Inc. may
 be used to endorse or promote products derived from the Apple Software
 without specific prior written permission from Apple.  Except as
 expressly stated in this notice, no other rights or licenses, express or
 implied, are granted by Apple herein, including but not limited to any
 patent rights that may be infringed by your derivative works or by other
 works in which the Apple Software may be incorporated.

 The Apple Software is provided by Apple on an "AS IS" basis.  APPLE
 MAKES NO WARRANTIES, EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION
 THE IMPLIED WARRANTIES OF NON-INFRINGEMENT, MERCHANTABILITY AND FITNESS
 FOR A PARTICULAR PURPOSE, REGARDING THE APPLE SOFTWARE OR ITS USE AND
 OPERATION ALONE OR IN COMBINATION WITH YOUR PRODUCTS.

 IN NO EVENT SHALL APPLE BE LIABLE FOR ANY SPECIAL, INDIRECT, INCIDENTAL
 OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 INTERRUPTION) ARISING IN ANY WAY OUT OF THE USE, REPRODUCTION,
 MODIFICATION AND/OR DISTRIBUTION OF THE APPLE SOFTWARE, HOWEVER CAUSED
 AND WHETHER UNDER THEORY OF CONTRACT, TORT (INCLUDING NEGLIGENCE),
 STRICT LIABILITY OR OTHERWISE, EVEN IF APPLE HAS BEEN ADVISED OF THE
 POSSIBILITY OF SUCH DAMAGE.

 Copyright (C) 2016 Apple Inc. All Rights Reserved.
*/

import UIKit

/**
    This delegate protocol is implemented so we can receive a callback when the
    thumbnail is loaded.
*/
protocol ThumbnailCacheDelegate: class {
    func thumbnailCache(_ thumbnailCache: ThumbnailCache, didLoadThumbnailsForURLs: Set<URL>)
}

/**
    The thumbnail cache class handles loading thumbnails, scaling the thumbnails
    to the propper size for our UI and informing its delegate once they're loaded.
*/
class ThumbnailCache {
    // MARK: - Properties

    fileprivate let cache: NSCache<AnyObject,AnyObject> = {
        let cache = NSCache<AnyObject,AnyObject>()
        
        cache.name = "jp.zenithgear.Lattices.thumbnailcache.cache"
        cache.countLimit = 64
        
        return cache
    }()

    fileprivate let workerQueue: OperationQueue = {
        let workerQueue = OperationQueue()
        
        workerQueue.name = "jp.zenithgear.Lattices.thumbnailcache.workerQueue"
        
        workerQueue.maxConcurrentOperationCount = ThumbnailCache.concurrentThumbnailOperations
        
        return workerQueue
    }()
    
    let thumbnailSize: CGSize
    
    fileprivate var URLsNeedingReload = Set<URL>()
    
    fileprivate var pendingThumbnails = [Int: Set<URL>]()
    
    fileprivate var cleanThumbnailDocumentIDs = Set<Int>()
    
    fileprivate var unscheduledDocumentIDs = [Int]()
    
    fileprivate var runningDocumentIDCount = 0
    
    fileprivate var scheduleSource: DispatchSource
    
    fileprivate var flushSource: DispatchSource
    
    weak var delegate: ThumbnailCacheDelegate?
    
    static let concurrentThumbnailOperations = 4

    // MARK: - Initialization

    init (thumbnailSize:CGSize) {
        self.thumbnailSize = thumbnailSize
        
        scheduleSource = DispatchSource.makeUserDataOrSource(queue: DispatchQueue.main) /*Migrator FIXME: Use DispatchSourceUserDataOr to avoid the cast*/ as! DispatchSource
        
        flushSource = DispatchSource.makeUserDataOrSource(queue: DispatchQueue.main) /*Migrator FIXME: Use DispatchSourceUserDataOr to avoid the cast*/ as! DispatchSource
        
        // Set up our scheduler which will manage an array of pending thumbnails
        scheduleSource.setEventHandler { [weak self] in
            guard let strongSelf = self else { return }
            
            strongSelf.scheduleThumbnailLoading()
        }
        
        scheduleSource.resume()
        
        // Set up our source which will push a batch of thumbnail updates at once.
        flushSource.setEventHandler { [weak self] in
            guard let strongSelf = self else { return }
            
            strongSelf.delegate?.thumbnailCache(strongSelf, didLoadThumbnailsForURLs: strongSelf.URLsNeedingReload)
            strongSelf.URLsNeedingReload.removeAll()
        }
        
        flushSource.resume()
    }

    // MARK: - Cache Management
    
    func markThumbnailCacheDirty() {
        // We've been asked to reload the UI and need to reload all items in the cache.
        cleanThumbnailDocumentIDs.removeAll()
    }

    func markThumbnailDirtyForURL(_ URL: Foundation.URL) {
        /*
            Mark the item dirty so that we know the next time we are asked for the
            thumbnail that we need to reload it.
        */
        if let documentIdentifier = documentIdentifierForURL(URL) {
            cleanThumbnailDocumentIDs.remove(documentIdentifier)
        }
    }

    func removeThumbnailForURL(_ URL: Foundation.URL) {
        /*
            Remove the item entirely from the cache because the item existing in the cache no
            longer makes sense for that URL.
        */
        if let documentIdentifier = documentIdentifierForURL(URL) {
            cache.removeObject(forKey: documentIdentifier as AnyObject)
            
            cleanThumbnailDocumentIDs.remove(documentIdentifier)
        }
    }
    
    func cancelThumbnailLoadForURL(_ URL: Foundation.URL) {
        if let documentIdentifier = documentIdentifierForURL(URL) {
            if let index = unscheduledDocumentIDs.index(of: documentIdentifier) {
                unscheduledDocumentIDs.remove(at: index)
                
                pendingThumbnails[documentIdentifier] = nil
            }
        }
    }
    

    // MARK: - Thumbnail Loading
    
    fileprivate func documentIdentifierForURL(_ URL: Foundation.URL) -> Int? {
        // Look up the document identifier on the URL which uniquely identifies a document.
        do {
            var documentIdentifier: AnyObject?
            try (URL as NSURL).getPromisedItemResourceValue(&documentIdentifier, forKey: URLResourceKey.documentIdentifierKey)
            
            return documentIdentifier as? Int
        }
        catch {
            return nil
        }
    }
    
    fileprivate func scheduleThumbnailLoading() {
        // While we have work left to schedule, schedule a thumbnail fetch in the background
        while self.runningDocumentIDCount < ThumbnailCache.concurrentThumbnailOperations {
            guard let nextDocumentID = self.unscheduledDocumentIDs.first else { break }
            
            let index = self.unscheduledDocumentIDs.index(of: nextDocumentID)!
            self.unscheduledDocumentIDs.remove(at: index)
            
            self.runningDocumentIDCount += 1
            
            let thumbnailURL = self.pendingThumbnails[nextDocumentID]!.first!
            
            let alreadyCached = self.cache.object(forKey: nextDocumentID as AnyObject) != nil ? true : false
            
            self.loadThumbnailInBackgroundForURL(thumbnailURL, documentIdentifier: nextDocumentID, alreadyCached: alreadyCached)
        }
    }
    
    fileprivate func loadThumbnailInBackgroundForURL(_ URL: Foundation.URL, documentIdentifier: Int, alreadyCached: Bool) {
        self.workerQueue.addOperation {
            if let thumbnail = self.loadThumbnailFromDiskForURL(URL) {
                // Scale the image to correct size.
                UIGraphicsBeginImageContextWithOptions(self.thumbnailSize, false, UIScreen.main.scale)
                
                let thumbnailRect = CGRect(x: 0, y: 0, width: self.thumbnailSize.width, height: self.thumbnailSize.height)
                
                thumbnail.draw(in: thumbnailRect)
                
                let scaledThumbnail = UIGraphicsGetImageFromCurrentImageContext()
                
                UIGraphicsEndImageContext()
                
                /*
                    Thumbnail loading succeeded. Save the thumbnail and call the
                    reload blocks to reload the UI.
                */
                self.cache.setObject(scaledThumbnail!, forKey: documentIdentifier as AnyObject)
                
                OperationQueue.main.addOperation {
                    self.cleanThumbnailDocumentIDs.insert(documentIdentifier)
                    
                    // Fetch all URLs for this `documentIdentifier`, not just the provided `URL` parameter.
                    let URLsForDocumentIdentifier = self.pendingThumbnails[documentIdentifier]!
                    
                    // Join the URLs for this identifier to any other URLs due for updating.
                    self.URLsNeedingReload.formUnion(URLsForDocumentIdentifier)
                    
                    self.pendingThumbnails[documentIdentifier] = nil
                    
                    // Trigger the event handler for the `flushSource` updating a batch of thumbnails.
                    self.flushSource.or(data: 1)

                    self.runningDocumentIDCount -= 1
                    
                    // Trigger the event handler for the `scheduleSource` scheduling thumbnail loading.
                    self.scheduleSource.or(data: 1)
                }
            }
            else {
                // Thumbnail loading failed. Just use the most recent cached thumbail.
                if !alreadyCached {
                    let image = UIImage(named: "MissingThumbnail.png")!
                    self.cache.setObject(image, forKey: documentIdentifier as AnyObject)
                }
                
                OperationQueue.main.addOperation {
                    self.cleanThumbnailDocumentIDs.insert(documentIdentifier)
                    
                    self.pendingThumbnails[documentIdentifier] = nil

                    self.runningDocumentIDCount -= 1
                    
                    // Trigger the event handler for the `scheduleSource` scheduling thumbnail loading.
                    self.scheduleSource.or(data: 1)
                }
            }
        }
    }
    
    func loadThumbnailForURL(_ URL: Foundation.URL) -> UIImage {
        /*
            We load the existing thumbnail (or a placeholder image if none has been
            loaded yet) and check if it is clean or not. If it isn't clean, we 
            load the thumbnail on a background queue to avoid blocking the main
            thread which could hamper scroll performance. Regardless of whether or
            not the thumbnail is clean, return the most up-to-date version of the
            thumbnail so we are sure to display something relatively up-to-date in 
            the UI.
        */
        
        /*
            We cache everything in our thumbnail cache by document identifier which
            is tracked properly accross renames.
        */
        guard let documentIdentifier = documentIdentifierForURL(URL) else {
            print("Failed to load docID and will display placeholder image for \(URL)")
            
            return UIImage(named: "MissingThumbnail.png")!
        }
        
        let existingImage = cache.object(forKey: documentIdentifier as AnyObject) as? UIImage
        
        if let existingImage = existingImage , cleanThumbnailDocumentIDs.contains(documentIdentifier) {
            // Everything fully up-to-date - return the cached image.
            return existingImage
        }

        // Use a placeholder image if one hasn't been loaded yet.
        let loadedThumbnail = existingImage ?? UIImage(named: "MissingThumbnail.png")!

        // If we are already loading that thumbnail, add our url to the reload list.
        if let URLs = pendingThumbnails[documentIdentifier] {
            pendingThumbnails[documentIdentifier] = URLs.union([URL])

            return loadedThumbnail
        }
        
        // Schedule the thumbnail to be loaded on a background queue.
        pendingThumbnails[documentIdentifier] = [URL]
        
        unscheduledDocumentIDs += [documentIdentifier]
        
        // Trigger the event handler for the `scheduleSource` scheduling thumbnail loading.
        scheduleSource.or(data: 1)
        
        // Return the most up-to-date image we have currently.
        return loadedThumbnail
    }
    
    fileprivate func loadThumbnailFromDiskForURL(_ URL: Foundation.URL) -> UIImage? {
        do {
            /*
                Load the thumbnail from disk.  Use getPromisedItemResourceValue because
                the document might not have been downloaded yet.
            */
            var thumbnailDictionary: AnyObject?
            try (URL as NSURL).getPromisedItemResourceValue(&thumbnailDictionary, forKey: URLResourceKey.thumbnailDictionaryKey)

            debugPrint("thumbnail url: \(URL)")

            /*
                We don't want to hang onto this in the URL cache because the URL 
                is long running and we maintain a separate cache for the thumbnails.
            */
            (URL as NSURL).removeCachedResourceValue(forKey: URLResourceKey.thumbnailDictionaryKey)
            
            guard let dictionary = thumbnailDictionary as? [String: UIImage],
                let image = dictionary[URLThumbnailDictionaryItem.NSThumbnail1024x1024SizeKey.rawValue] else {
                    throw ShapeEditError.thumbnailLoadFailed
            }
            
            return image
        }
        catch {
            return nil
        }
    }
}
