//
//  AlbumCollection.swift
//  TagTunes
//
//  Created by Kim Wittenburg on 08.09.15.
//  Copyright © 2015 Kim Wittenburg. All rights reserved.
//

import Foundation

/// Manages a collection of albums. Managing includes support for deferred
/// loading of an album's tracks as well as error support.
public class AlbumCollection: CollectionType {
    
    // MARK: Types
    
    private enum AlbumState {
        
        case Normal
        
        case Error(NSError)
        
        case Loading(NSURLSessionTask)
        
    }
    
    /// Notifications posted by an album collection. The `userInfo` of these
    /// notifications contains all keys specified in `Keys`.
    public struct Notifications {
        
        /// Posted when an album is added to a collection. This notification is
        /// only posted if the album collection actually changed.
        public static let albumAdded = "AlbumAddedNotificationName"
        
        /// Posted when an album is removed from a collection. This notification
        /// is only posted if the album collection actually changed.
        public static let albumRemoved = "AlbumRemovedNotificationName"
        
        /// Posted when the album collection started a network request for an
        /// album's tracks.
        ///
        /// Note that the values for the keys `Album` and `AlbumIndex` for the
        /// corresponding `AlbumFinishedLoading` notification may both be
        /// different.
        public static let albumStartedLoading = "AlbumStartedLoadingNotificationName"
        
        /// Posted when an album collection finished loading the tracks for an
        /// album. Receiving this notification does not mean that the tracks were
        /// actually loaded successfully. It just means that the networ
        /// connection terminated. Use `errorForAlbum` to determine if an error
        /// occured while the tracks have been loaded.
        ///
        /// - note: Since the actual `Album` instance in the album collection may
        /// change during loading its tracks it is preferred that you use the
        /// `AlbumIndexKey` of the notification to determine which album finished
        /// loading its tracks. You can however use the `AlbumKey` as well to
        /// access the `Album` instance that is currently present in the
        /// collection at the respective index.
        public static let albumFinishedLoading = "AlbumFinishedLoadingNotificationName"

        /// These constants are available as keys in the `userInfo` dictionary
        /// for any notification an album collection may post.
        public struct Keys {
            
            /// The `Album` instance affected by the notification.
            public static let album = "AlbumKey"
            
            /// The index of the album affected by the notification.
            public static let albumIndex = "AlbumIndexKey"
            
        }
        
    }
    
    // MARK: Properties
    
    /// Access the userlying array of albums.
    public private(set) var albums = [Album]()
    
    private var albumStates = [Album: AlbumState]()
    
    /// The URL session used to load tracks for albums.
    private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
    
    // MARK: Collection Type
    
    public init() {}
    
    public var startIndex: Int {
        return albums.startIndex
    }
    
    public var endIndex: Int {
        return albums.endIndex
    }

    public subscript (position: Int) -> Album {
        return albums[position]
    }
    
    // MARK: Album Collection
    
    /// Adds the specified album, if not already present, and begins to load its
    /// tracks.
    ///
    /// - parameters:
    ///   - album: The album to be added.
    ///   - flag: Specify `false` if the album collection should not begin to
    ///           load the album's tracks immediately.
    public func addAlbum(album: Album, beginLoading flag: Bool = true) {
        if !albums.contains(album) {
            albums.append(album)
            let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: albums.count-1]
            NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumAdded, object: self, userInfo: userInfo)
            if flag {
                beginLoadingTracksForAlbum(album)
            }
        }
    }
    
    /// Removes the specified album from the collection if it is present.
    public func removeAlbum(album: Album) {
        if let index = albums.indexOf(album) {
            removeAlbumAtIndex(index)
        }
    }
    
    /// Removes the album at the specified index from the collection.
    ///
    /// - requires: The specified index must be in the collection's range.
    public func removeAlbumAtIndex(index: Int) {
        let album = self[index]
        setAlbumState(nil, forAlbum: album)
        albums.removeAtIndex(index)
        let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: index]
        NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumRemoved, object: self, userInfo: userInfo)
    }
    
    /// Begins to load the tracks for the specified album. If there is already a
    /// request for the specified album it is cancelled. When the tracks for the
    /// specified album have been loaded or an error occured, a
    /// `AlbumFinishedLoadingNotification` is posted.
    public func beginLoadingTracksForAlbum(album: Album) {
        guard let albumIndex = albums.indexOf(album) else {
            return
        }
        let url = iTunesAPI.createAlbumLookupURLForId(album.id)
        let task = urlSession.dataTaskWithURL(url) { (data, response, error) -> Void in
            var newAlbumIndex = self.albums.indexOf(album)!
            defer {
                let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: self.albums[albumIndex], AlbumCollection.Notifications.Keys.albumIndex: albumIndex]
                NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumFinishedLoading, object: self, userInfo: userInfo)
            }
            guard error == nil else {
                if error!.code != NSUserCancelledError {
                    self.albumStates[album] = .Error(error!)
                }
                return
            }
            do {
                let newAlbum = try iTunesAPI.parseAPIData(data!)[0]
                self.albums.removeAtIndex(newAlbumIndex)
                self.albums.insert(newAlbum, atIndex: newAlbumIndex)
                self.setAlbumState(.Normal, forAlbum: album)
            } catch let error as NSError {
                self.setAlbumState(.Error(error), forAlbum: album)
            } catch _ {
                // Will never happen
            }
        }
        setAlbumState(.Loading(task), forAlbum: album)
        task.resume()
        let userInfo: [NSObject: AnyObject] = [AlbumCollection.Notifications.Keys.album: album, AlbumCollection.Notifications.Keys.albumIndex: albumIndex]
        NSNotificationCenter.defaultCenter().postNotificationName(AlbumCollection.Notifications.albumStartedLoading, object: self, userInfo: userInfo)

    }
    
    /// Cancels the request to load the tracks for the specified album and sets
    /// the error for the album to a `NSUserCancelledError` in the
    /// `NSCocoaErrorDomain`.
    public func cancelLoadingTracksForAlbum(album: Album) {
        let error = NSError(domain: NSCocoaErrorDomain, code: NSUserCancelledError, userInfo: nil)
        setAlbumState(.Error(error), forAlbum: album)
    }
    
    /// Sets the state for the specified album. If the previous state was
    /// `Loading` the associated task is cancelled.
    private func setAlbumState(state: AlbumState?, forAlbum album: Album) {
        if case let .Some(.Loading(task)) = albumStates[album] {
            task.cancel()
        }
        albumStates[album] = state
    }
    
    public func isAlbumLoading(album: Album) -> Bool {
        if case .Some(.Loading) = albumStates[album] {
            return true
        }
        return false
    }
    
    public func errorForAlbum(album: Album) -> NSError? {
        if case let .Some(.Error(error)) = albumStates[album] {
            return error
        }
        return nil
    }
    
}
