//
//  ViewController.swift
//  TagTunes
//
//  Created by Kim Wittenburg on 28.08.15.
//  Copyright © 2015 Kim Wittenburg. All rights reserved.
//

import Cocoa
import AppKitPlus

internal class MainViewController: NSViewController {
    
    // MARK: Types
    
    private struct OutlineViewConstants {
        struct ViewIdentifiers {
            static let simpleTableCellViewIdentifier = "SimpleTableCellViewIdentifier"
            static let centeredTableCellViewIdentifier = "CenteredTableCellViewIdentifier"
            static let albumTableCellViewIdentifier = "AlbumTableCellViewIdentifier"
            static let trackTableCellViewIdentifier = "TrackTableCellViewIdentifier"
        }
        
        struct Items {
            static let loadingItem: AnyObject = "LoadingItem"
            static let noResultsItem: AnyObject = "NoResultsItem"
            
            static let searchResultsHeaderItem: AnyObject = MainViewController.Section.SearchResults.rawValue
            static let albumsHeaderItem: AnyObject = MainViewController.Section.Albums.rawValue
            static let unsortedTracksHeaderItem: AnyObject = MainViewController.Section.UnsortedTracks.rawValue
        }
        
        static let pasteboardType = "public.item.tagtunes"
    }
    
    private struct KVOContexts {
        static var preferencesContext = "KVOPreferencesContext"
    }
    
    internal enum Section: String {
        case SearchResults = "SearchResults"
        case Albums = "Albums"
        case UnsortedTracks = "UnsortedTracks"
        
        static func isHeaderItem(item: AnyObject) -> Bool {
            if let itemAsString = item as? String {
                return Section(rawValue: itemAsString) != nil
            } else {
                return false
            }
        }
    }

    // MARK: IBOutlets
    
    @IBOutlet private weak var outlineView: NSOutlineView!
    
    // MARK: Properties
    
    /// Used for searching and loading search results
    private let urlSession = NSURLSession(configuration: NSURLSessionConfiguration.defaultSessionConfiguration(), delegate: nil, delegateQueue: NSOperationQueue.mainQueue())
    
    /// The URL task currently loading the search results
    private var searchTask: NSURLSessionTask?
    
    private var searchTerm: String?
    
    /// If `true` the search section is displayed at the top of the
    /// `outlineView`.
    internal var showsSearch: Bool = false
    
    /// `true` if there is currently a search in progress.
    internal var searching: Bool {
        return searchTask != nil
    }
    
    /// The error that occured during searching, if any.
    internal private(set) var searchError: NSError?
    
    // MARK: View Life Cycle
    
    // Proxy objects that act as `NSNotificationCenter` observers.
    private var observerProxies = [NSObjectProtocol]()
    
    override internal func viewDidLoad() {
        super.viewDidLoad()
        
        let startedLoadingTracksObserver = NSNotificationCenter.defaultCenter().addObserverForName(AlbumCollection.Notifications.albumStartedLoading, object: albumCollection, queue: NSOperationQueue.mainQueue(), usingBlock: albumCollectionDidBeginLoadingTracks)
        let finishedLoadingTracksObserver = NSNotificationCenter.defaultCenter().addObserverForName(AlbumCollection.Notifications.albumFinishedLoading, object: albumCollection, queue: NSOperationQueue.mainQueue(), usingBlock: albumCollectionDidFinishLoadingTracks)

        observerProxies.append(startedLoadingTracksObserver)
        observerProxies.append(finishedLoadingTracksObserver)
        
        Preferences.sharedPreferences.addObserver(self, forKeyPath: "useCensoredNames", options: [], context: &KVOContexts.preferencesContext)
        Preferences.sharedPreferences.addObserver(self, forKeyPath: "caseSensitive", options: [], context: &KVOContexts.preferencesContext)
        
        outlineView.setDraggingSourceOperationMask(.Move, forLocal: true)
        outlineView.registerForDraggedTypes([OutlineViewConstants.pasteboardType])
    }
    
    deinit {
        for observer in observerProxies {
            NSNotificationCenter.defaultCenter().removeObserver(observer)
        }
    }
    
    // MARK: Outline View Content
    
    internal private(set) var searchResults = [SearchResult]()
    
    internal let albumCollection = AlbumCollection()
    
    internal private(set) var unsortedTracks = [iTunesTrack]()
    
    /// Returns all `iTunesTrack` objects that are somewhere down the outline
    /// view.
    private var allITunesTracks: Set<iTunesTrack> {
        return Set(unsortedTracks).union(albumCollection.flatMap({ $0.tracks.flatMap { $0.associatedTracks } }))
    }
    
    /// Returns all contents of the outline view.
    ///
    /// This property is regenerated every time it is queried. If you need to
    /// access it a lot of times it is recommended to chache it into a local
    /// variable.
    private var outlineViewContents: [AnyObject] {
        var contents = [AnyObject]()
        if showsSearch {
            contents.append(OutlineViewConstants.Items.searchResultsHeaderItem)
            if !searchResults.isEmpty {
                contents.appendContentsOf(searchResults as [AnyObject])
            } else if searching {
                contents.append(OutlineViewConstants.Items.loadingItem)
            } else if let error = searchError {
                contents.append(error)
            } else {
                contents.append(OutlineViewConstants.Items.noResultsItem)
            }
            if !albumCollection.isEmpty {
                contents.append(OutlineViewConstants.Items.albumsHeaderItem)
            }
        }
        contents.appendContentsOf(albumCollection.albums as [AnyObject])
        if !unsortedTracks.isEmpty {
            contents.append(OutlineViewConstants.Items.unsortedTracksHeaderItem)
            contents.appendContentsOf(unsortedTracks as [AnyObject])
        }
        return contents
    }
    
    /// Returns all selected items. This property removes duplicate items from
    /// the returned array (for example a track is not included if the whole
    /// album the track belongs to is included itself).
    ///
    /// This value is not cached. If you need to access this value often, you
    /// should consider caching it yourself in a local variable. The order in
    /// which the selected objects occur in the returned array is random.
    ///
    /// - returns: An array of `SearchResult`s, `Album`s, `Track`s and
    /// `iTunesTrack`s.
    private var selectedItems: [AnyObject] {
        var selectedSearchResults = Set<SearchResult>()
        var selectedAlbums = Set<Album>()
        var selectedTracks = Set<Track>()
        var selectedITunesTracks = Set<iTunesTrack>()
        
        for row in outlineView.selectedRowIndexes {
            let item = outlineView.itemAtRow(row)
            if let searchResult = item as? SearchResult {
                selectedSearchResults.insert(searchResult)
            } else if let album = item as? Album {
                selectedAlbums.insert(album)
            } else if let track = item as? Track {
                selectedTracks.insert(track)
            } else if let track = item as? iTunesTrack {
                selectedITunesTracks.insert(track)
            }
        }
        
        for album in selectedAlbums {
            for track in album.tracks {
                for iTunesTrack in track.associatedTracks {
                    selectedITunesTracks.remove(iTunesTrack)
                }
                selectedTracks.remove(track)
            }
        }
        
        for track in selectedTracks {
            for iTunesTrack in track.associatedTracks {
                selectedITunesTracks.remove(iTunesTrack)
            }
        }
        
        var selectedItems = [AnyObject]()
        selectedItems.appendContentsOf(Array(selectedSearchResults) as [AnyObject])
        selectedItems.appendContentsOf(Array(selectedAlbums) as [AnyObject])
        selectedItems.appendContentsOf(Array(selectedTracks) as [AnyObject])
        selectedItems.appendContentsOf(Array(selectedITunesTracks) as [AnyObject])
        return selectedItems
    }
    
    internal func parentForTrack(track: iTunesTrack) -> Track? {
        return outlineView.parentForItem(track) as? Track
    }
    
    internal func containsAlbumForSearchResult(searchResult: SearchResult) -> Bool {
        for album in albumCollection {
            if album.id == searchResult.id {
                return true
            }
        }
        return false
    }
    
    /// Returns the section of the specified row or `nil` if the row is not a
    /// valid row.
    internal func sectionOfRow(row: Int) -> Section? {
        if row < 0 {
            return nil
        }
        var relativeRow = row
        if showsSearch {
            let searchRelatedItemCount = 1 + (searchResults.isEmpty ? 1 : searchResults.count)
            if relativeRow < searchRelatedItemCount {
                return .SearchResults
            } else {
                relativeRow -= searchRelatedItemCount
            }
        }
        var maxRow = outlineView.numberOfRows
        if !unsortedTracks.isEmpty {
            maxRow -= unsortedTracks.count + 1
        }
        if relativeRow < maxRow {
            return .Albums
        } else {
            relativeRow -= maxRow
        }
        if relativeRow < unsortedTracks.count + 1 {
            return .UnsortedTracks
        }
        return nil
    }
    
    // MARK: Searching
    
    /// Starts a search for the specified search term. Calling this method
    internal func beginSearchForTerm(term: String) {
        cancelSearch()
        if let url = iTunesAPI.createAlbumSearchURLForTerm(term) {
            showsSearch = true
            searchTerm = term
            searchTask = urlSession.dataTaskWithURL(url, completionHandler: processSearchResults)
            searchTask?.resume()
        } else {
            showsSearch = false
        }
        outlineView.reloadData()
    }
    
    /// Cancels the current search (if there is one). This also hides the search
    /// results.
    internal func cancelSearch() {
        searchTask?.cancel()
        searchResults.removeAll()
        showsSearch = false
        outlineView.reloadData()
    }
    
    /// Processes the data returned from a network request into the
    /// `searchResults`.
    private func processSearchResults(data: NSData?, response: NSURLResponse?, var error: NSError?) {
        searchTask = nil
        if let theData = data where error == nil {
            do {
                let searchResults = try iTunesAPI.parseAPIData(theData).map { SearchResult(representedAlbum: $0) }
                searchTerm = nil
                self.searchResults = searchResults
            } catch let theError as NSError {
                error = theError
            }
        }
        if let theError = error {
            searchErrorOccured(theError)
        }
        showsSearch = true
        outlineView.reloadData()
    }
    
    /// Called when an error occurs during searching.
    private func searchErrorOccured(error: NSError) {
        searchError = error
    }
    
    /// Adds the search result at the specified `row` to the albums section and
    /// begins loading its tracks.
    internal func selectSearchResultAtRow(row: Int) {
        guard sectionOfRow(row) == .SearchResults else {
            return
        }
        let searchResult = outlineView.itemAtRow(row) as! SearchResult
        if !Preferences.sharedPreferences.keepSearchResults {
            searchResults.removeAll()
            showsSearch = false
        }
        if !containsAlbumForSearchResult(searchResult) {
            let album = Album(searchResult: searchResult)
            albumCollection.addAlbum(album, beginLoading: true)
        }
        outlineView.reloadData()
    }
    
    // MARK: Saving
    
    private func saveItems(items: [AnyObject]) {
        let numberOfTracks = numberOfTracksInItems(items)
        let progress = NSProgress(totalUnitCount: Int64(numberOfTracks))
        NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving tracks…", comment: "Alert message indicating that the selected tracks are currently being saved")
        NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
        
        let save: (parentTrack: Track, tracks: [iTunesTrack]) -> Bool = { parentTrack, tracks in
            for track in tracks {
                if progress.cancelled {
                    return false
                }
                parentTrack.saveToTrack(track)
                ++progress.completedUnitCount
                NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
            }
            return !progress.cancelled
        }
        
        for item in items {
            if let album = item as? Album {
                for parentTrack in album.tracks {
                    if !save(parentTrack: parentTrack, tracks: parentTrack.associatedTracks) {
                        return
                    }
                }
            } else if let track = item as? Track {
                if !save(parentTrack: track, tracks: track.associatedTracks) {
                    return
                }
            } else if let track = item as? iTunesTrack {
                if let parentTrack = parentForTrack(track) {
                    if !save(parentTrack: parentTrack, tracks: [track]) {
                        return
                    }
                }
            }
        }
    }
    
    private func numberOfTracksInItems(items: [AnyObject]) -> Int {
        return items.reduce(0) { (count: Int, item: AnyObject) -> Int in
            if let album = item as? Album {
                return count + album.tracks.reduce(0) { $0 + $1.associatedTracks.count }
            } else if let track = item as? Track {
                return count + track.associatedTracks.count
            } else if let track = item as? iTunesTrack {
                return parentForTrack(track) == nil ? count : count + 1
            } else {
                return count
            }
        }
    }
    
    private func saveArtworksForItems(items: [AnyObject]) {
        var albums = Set<Album>()
        for item in items {
            if let searchResult = item as? SearchResult {
                albums.insert(Album(searchResult: searchResult))
            } else if let album = item as? Album {
                albums.insert(album)
            } else if let track = item as? Track {
                albums.insert(track.album)
            } else if let track = item as? iTunesTrack {
                if let parentTrack = parentForTrack(track) {
                    albums.insert(parentTrack.album)
                }
            }
        }
        
        let progress = NSProgress(totalUnitCount: Int64(albums.count))
        var errorCount = 0
        NSProgress.currentProgress()?.localizedDescription = NSLocalizedString("Saving artworks…", comment: "Alert message indicating that the artworks for the selected tracks are currently being saved")
        NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
        NSThread.sleepForTimeInterval(2)
        for album in albums {
            if progress.cancelled {
                return
            }
            do {
                try album.saveArtwork()
            } catch _ {
                ++errorCount
            }
            ++progress.completedUnitCount
            NSProgress.currentProgress()?.localizedAdditionalDescription = progress.localizedAdditionalDescription
        }
        if errorCount > 0 {
            dispatch_sync(dispatch_get_main_queue()) {
                let alert = NSAlert()
                alert.messageText = String(format: NSLocalizedString("%d artworks could not be saved.", comment: "Error message indicating that n artworks could not be saved."), errorCount)
                alert.informativeText = NSLocalizedString("Please check your privileges for the folder you set in the preferences and try again.", comment: "Informative text for 'artwork(s) could not be saved' errors")
                alert.alertStyle = .WarningAlertStyle
                alert.addButtonWithTitle("OK")
                alert.beginSheetModalForWindow(self.view.window!, completionHandler: nil)
            }
        }
    }
    
    // MARK: Notifications
    
    private func albumCollectionDidBeginLoadingTracks(notification: NSNotification) {
        outlineView.reloadData()
    }
    
    private func albumCollectionDidFinishLoadingTracks(notification: NSNotification) {
        outlineView.reloadData()
    }

    override internal func observeValueForKeyPath(keyPath: String?, ofObject object: AnyObject?, change: [String : AnyObject]?, context: UnsafeMutablePointer<Void>) {
        if context == &KVOContexts.preferencesContext {
            outlineView.reloadData()
        }
    }
    
    // MARK: Actions
    
    /// Adds the current iTunes selection
    @IBAction internal func addITunesSelection(sender: AnyObject?) {
        if !iTunes.running {
            let alert = NSAlert()
            alert.messageText = NSLocalizedString("iTunes is not running", comment: "Error message informing the user that iTunes is not currently running.")
            alert.informativeText = NSLocalizedString("Please launch iTunes and try again.", comment: "Informative text for the 'iTunes is not running' error")
            alert.addButtonWithTitle(NSLocalizedString("OK", comment: "Button title"))
            alert.beginSheetModalForWindow(view.window!, completionHandler: nil)
        } else if let selection = iTunes.selection.get() as? [iTunesTrack] {
            let newTracks = Set(selection).subtract(allITunesTracks)
            unsortedTracks.appendContentsOf(newTracks)
            outlineView.reloadData()
        }
    }

    /// Begins to search for the `sender`'s `stringValue`.
    @IBAction internal func performSearch(sender: AnyObject?) {
        if let searchTerm = sender?.stringValue {
            beginSearchForTerm(searchTerm)
        }
    }
    
    /// Selects the search result associated with the `sender`'s row (as
    /// determined by `NSOutlineView.rowForView`) and adds it to the list of
    /// albums.
    @IBAction private func selectSearchResult(sender: AnyObject?) {
        if let view = sender as? NSView {
            let row = outlineView.rowForView(view)
            selectSearchResultAtRow(row)
        }
    }

    /// Saves the selected items to iTunes. The saving process will be reported
    /// to the user in a progress sheet.
    @IBAction internal func performSave(sender: AnyObject?) {
        let selectedItems = self.selectedItems.filter { !($0 is SearchResult) }
        let progress = NSProgress(totalUnitCount: 100)
        let progressAlert = ProgressAlert(progress: progress)
        progressAlert.dismissesWhenCancelled = false
        progressAlert.beginSheetModalForWindow(self.view.window!) {
            response in
            self.outlineView.reloadData()
        }
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
            if Preferences.sharedPreferences.saveArtwork {
                progress.becomeCurrentWithPendingUnitCount(90)
            } else {
                progress.becomeCurrentWithPendingUnitCount(100)
            }
            self.saveItems(selectedItems)
            progress.resignCurrent()
            if progress.cancelled {
                progressAlert.dismissWithResponse(NSModalResponseAbort)
                return
            }
            if Preferences.sharedPreferences.saveArtwork {
                progress.becomeCurrentWithPendingUnitCount(10)
                self.saveArtworksForItems(selectedItems)
                progress.resignCurrent()
                if progress.cancelled {
                    progressAlert.dismissWithResponse(NSModalResponseAbort)
                }
            }
        }
    }

    /// Saves the artworks of the selected items to the folder specified in the
    /// preferences. If there is no folder specified this method prompts the user
    /// to select one.
    @IBAction internal func saveArtworks(sender: AnyObject?) {
        if Preferences.sharedPreferences.artworkTarget == nil {
            let alert = NSAlert()
            alert.messageText = NSLocalizedString("There is no folder set to save artworks to.", comment: "Error message informing the user that there is no directory set in the preferences that can be used to save artworks to.")
            alert.informativeText = NSLocalizedString("You must select a folder to save artworks to. The folder can be changed in the preferences.", comment: "Informative text for the 'no folder to save artworks to' error.")
            alert.addButtonWithTitle(NSLocalizedString("Use Downloads Folder", comment: "Button title offering the user to automatically use the downloads directory instead of manually choosing a directory."))
            alert.addButtonWithTitle(NSLocalizedString("Chose Folder…", comment: "Button title prompting the user to choose a folder."))
            alert.addButtonWithTitle(NSLocalizedString("Cancel", comment: "Button title"))
            alert.alertStyle = .WarningAlertStyle
            alert.beginSheetModalForWindow(view.window!) { response in
                switch response {
                case NSAlertFirstButtonReturn:
                    let downloadsFolder = NSURL.fileURLWithPath(NSFileManager.defaultManager().URLsForDirectory(.DownloadsDirectory, inDomains: .UserDomainMask)[0].filePathURL!.path!, isDirectory: true)
                    Preferences.sharedPreferences.artworkTarget = downloadsFolder
                    self.performSaveArtworks()
                case NSAlertSecondButtonReturn:
                    let openPanel = NSOpenPanel()
                    openPanel.canChooseDirectories = true
                    openPanel.canChooseFiles = false
                    openPanel.canCreateDirectories = true
                    openPanel.prompt = NSLocalizedString("Choose…", comment: "Button title in an open dialog prompting the user to choose a directory")
                    openPanel.beginSheetModalForWindow(self.view.window!) { response in
                        if response == NSFileHandlingPanelOKButton {
                            Preferences.sharedPreferences.artworkTarget = openPanel.URL!.filePathURL!
                            self.performSaveArtworks()
                        }
                    }
                case NSAlertThirdButtonReturn:
                    fallthrough
                default:
                    return
                }
            }
        } else {
            performSaveArtworks()
        }
    }
    
    /// Actually performs the action for `saveArtworks`.
    private func performSaveArtworks() {
        let progress = NSProgress(totalUnitCount: 100)
        let progressAlert = ProgressAlert(progress: progress)
        progressAlert.dismissesWhenCancelled = false
        progressAlert.beginSheetModalForWindow(self.view.window!) {
            response in
            self.outlineView.reloadData()
        }
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0)) {
            progress.becomeCurrentWithPendingUnitCount(100)
            self.saveArtworksForItems(self.selectedItems)
            progress.resignCurrent()
            if progress.cancelled {
                progressAlert.dismissWithResponse(NSModalResponseAbort)
            }
        }
    }
    
    /// Removes the selected items from the outline view.
    @IBAction internal func removeSelectedItems(sender: AnyObject?) {
        let items = outlineView.selectedRowIndexes.map { ($0, outlineView.itemAtRow($0)) }
        for (row, item) in items {
            if sectionOfRow(row)! != .SearchResults {
                if let album = item as? Album {
                    albumCollection.removeAlbum(album)
                } else if let track = item as? Track {
                    track.associatedTracks = []
                } else if let track = item as? iTunesTrack {
                    if let parentTrack = parentForTrack(track) {
                        parentTrack.associatedTracks.removeElement(track)
                    } else {
                        unsortedTracks.removeElement(track)
                    }
                }
            }
        }
        outlineView.reloadData()
    }
    
    /// Action that should be triggered from a view inside the outline view. If
    /// `sender` is not an `NSError` instance the item at the row associated with
    /// the `sender` (as determined by `NSOutlineView.rowForView`) should be a
    /// `NSError` or `Album` instance for this method to work correctly.
    @IBAction private func showErrorDetails(sender: AnyObject?) {
        var error: NSError
        if let theError = sender as? NSError {
            error = theError
        } else if let view = sender as? NSView {
            let row = outlineView.rowForView(view)
            let item = outlineView.itemAtRow(row)
            if let theError = item as? NSError {
                error = theError
            } else if let album = item as? Album {
                if let theError = albumCollection.errorForAlbum(album) {
                    error = theError
                } else {
                    return
                }
            } else {
                return
            }
        } else {
            return
        }
        presentError(error, modalForWindow: view.window!, delegate: nil, didPresentSelector: nil, contextInfo: nil)
    }
    
}

// MARK: - Error Handling

extension MainViewController {
    
    override internal func willPresentError(error: NSError) -> NSError {
        let recoveryOptions = [
            NSLocalizedString("OK", comment: "Button title"),
            NSLocalizedString("Try Again", comment: "Button title for error alerts offering the user to try again.")
        ]
        return DescriptiveError(underlyingError: error, userInfo: [NSRecoveryAttempterErrorKey: self, NSLocalizedRecoveryOptionsErrorKey: recoveryOptions])
    }
    
    override internal func attemptRecoveryFromError(error: NSError, optionIndex recoveryOptionIndex: Int, delegate: AnyObject?, didRecoverSelector: Selector, contextInfo: UnsafeMutablePointer<Void>) {
        let didRecover = attemptRecoveryFromError(error, optionIndex: recoveryOptionIndex)
        delegate?.performSelector(didRecoverSelector, withObject: didRecover, withObject: contextInfo as! AnyObject)
    }
    
    override internal func attemptRecoveryFromError(error: NSError, optionIndex recoveryOptionIndex: Int) -> Bool {
        if recoveryOptionIndex == 0 {
            return true
        }
        if let term = searchTerm where error == searchError || error.userInfo[NSUnderlyingErrorKey] === searchError {
            beginSearchForTerm(term)
            return true
        } else {
            for album in albumCollection {
                let albumError = albumCollection.errorForAlbum(album)
                if error == albumError || error.userInfo[NSUnderlyingErrorKey] === albumError {
                    albumCollection.beginLoadingTracksForAlbum(album)
                    return true
                }
            }
        }
        return false
    }
    
}

// MARK: - User Interface Validations

extension MainViewController: NSUserInterfaceValidations {
    
    internal func validateUserInterfaceItem(anItem: NSValidatedUserInterfaceItem) -> Bool {
        if anItem.action() == "performSave:" {
            return canSave()
        } else if anItem.action() == "saveArtworks:" {
            return canSaveArtworks()
        } else if anItem.action() == "addITunesSelection:" {
            return canAddITunesSelection()
        } else if anItem.action() == "removeSelectedItems:" {
            return canRemoveSelectedItems()
        }
        return false
    }
    
    private func canSave() -> Bool {
        for row in outlineView.selectedRowIndexes where sectionOfRow(row) == .Albums {
            let item = outlineView.itemAtRow(row)
            if let album = item as? Album {
                if !album.saved {
                    return true
                }
            } else if let track = item as? Track {
                if !track.saved {
                    return true
                }
            } else if let track = item as? iTunesTrack {
                if parentForTrack(track)?.saved == false {
                    return true
                }
            }
        }
        return false
    }
    
    private func canSaveArtworks() -> Bool {
        for row in outlineView.selectedRowIndexes {
            if sectionOfRow(row) != .UnsortedTracks {
                return true
            }
        }
        return false
    }
    
    private func canAddITunesSelection() -> Bool {
        return iTunes.running && !(iTunes.selection.get() as! [AnyObject]).isEmpty
    }
    
    private func canRemoveSelectedItems() -> Bool {
        for row in outlineView.selectedRowIndexes {
            if sectionOfRow(row)! != .SearchResults {
                return true
            }
        }
        return false
    }
    
}

// MARK: - Outline View

extension MainViewController: NSOutlineViewDataSource, NSOutlineViewDelegate {
    
    internal func outlineView(outlineView: NSOutlineView, numberOfChildrenOfItem item: AnyObject?) -> Int {
        if item == nil {
            return outlineViewContents.count
        } else if let album = item as? Album {
            return album.tracks.count
        } else if let track = item as? Track {
            return track.associatedTracks.count
        } else {
            return 0
        }
    }
    
    internal func outlineView(outlineView: NSOutlineView, child index: Int, ofItem item: AnyObject?) -> AnyObject {
        if item == nil {
            return outlineViewContents[index]
        } else if let album = item as? Album {
            return album.tracks[index]
        } else if let track = item as? Track {
            return track.associatedTracks[index]
        } else {
            return ""
        }
    }
    
    internal func outlineView(outlineView: NSOutlineView, isItemExpandable item: AnyObject) -> Bool {
        return self.outlineView(outlineView, numberOfChildrenOfItem: item) > 0
    }
    
    internal func outlineView(outlineView: NSOutlineView, isGroupItem item: AnyObject) -> Bool {
        return Section.isHeaderItem(item)
    }
    
    internal func outlineView(outlineView: NSOutlineView, shouldSelectItem item: AnyObject) -> Bool {
        return !(self.outlineView(outlineView, isGroupItem: item) || item === OutlineViewConstants.Items.loadingItem || item === OutlineViewConstants.Items.noResultsItem || item is NSError)
    }
    
    internal func outlineView(outlineView: NSOutlineView, heightOfRowByItem item: AnyObject) -> CGFloat {
        if item is Album || item is SearchResult {
            return 39
        } else if let track = item as? Track {
            return track.album.hasSameArtistNameAsTracks ? 24 : 31
        } else if item === OutlineViewConstants.Items.loadingItem {
            return 39
        } else if item === OutlineViewConstants.Items.noResultsItem || item is NSError {
            return 32
        } else if Section.isHeaderItem(item) {
            return 24
        } else {
            return 17
        }
    }
    
    internal func outlineView(outlineView: NSOutlineView, viewForTableColumn tableColumn: NSTableColumn?, item: AnyObject) -> NSView? {
        if item === OutlineViewConstants.Items.searchResultsHeaderItem {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
            if view == nil {
                view = AdvancedTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier
            }
            view?.style = .Simple
            view?.textField?.font = NSFont.boldSystemFontOfSize(0)
            view?.textField?.textColor = NSColor.disabledControlTextColor()
            view?.textField?.stringValue = NSLocalizedString("Search Results", comment: "Header name for the seach results section")
            return view
        }
        if item === OutlineViewConstants.Items.albumsHeaderItem {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
            if view == nil {
                view = AdvancedTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier
            }
            view?.style = .Simple
            view?.textField?.font = NSFont.boldSystemFontOfSize(0)
            view?.textField?.textColor = NSColor.disabledControlTextColor()
            view?.textField?.stringValue = NSLocalizedString("Albums", comment: "Header name for the albums section")
            return view
        }
        if item === OutlineViewConstants.Items.unsortedTracksHeaderItem {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
            if view == nil {
                view = AdvancedTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier
            }
            view?.style = .Simple
            view?.textField?.font = NSFont.boldSystemFontOfSize(0)
            view?.textField?.textColor = NSColor.disabledControlTextColor()
            view?.textField?.stringValue = NSLocalizedString("Unsorted Tracks", comment: "Header name for the unsorted tracks section")
            return view
        }
        if item === OutlineViewConstants.Items.loadingItem {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView
            if view == nil {
                view = CenteredTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier
            }
            view?.setupForLoading()
            return view
        }
        if item === OutlineViewConstants.Items.noResultsItem {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView
            if view == nil {
                view = CenteredTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier
            }
            view?.setupForMessage(NSLocalizedString("No Results", comment: "Message informing the user that the search didn't return any results"))
            return view
        }
        if let error = item as? NSError {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier, owner: nil) as? CenteredTableCellView
            if view == nil {
                view = CenteredTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.centeredTableCellViewIdentifier
            }
            view?.button?.target = self
            view?.button?.action = "showErrorDetails:"
            view?.setupForError(error, errorMessage: NSLocalizedString("Failed to load results", comment: "Error message informing the user that an error occured during searching."))
            return view
        }
        if let searchResult = item as? SearchResult {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
            if view == nil {
                view = AlbumTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier
            }
            view?.button.target = self
            view?.button.action = "selectSearchResult:"
            let selectable = !containsAlbumForSearchResult(searchResult)
            view?.setupForSearchResult(searchResult, selectable: selectable)
            return view
        }
        if let album = item as? Album {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier, owner: nil) as? AlbumTableCellView
            if view == nil {
                view = AlbumTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.albumTableCellViewIdentifier
            }
            view?.setupForAlbum(album, loading: albumCollection.isAlbumLoading(album), error: albumCollection.errorForAlbum(album))
            view?.errorButton?.target = self
            view?.errorButton?.action = "showErrorDetails:"
            return view
        }
        if let track = item as? Track {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.trackTableCellViewIdentifier, owner: nil) as? TrackTableCellView
            if view == nil {
                view = TrackTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.trackTableCellViewIdentifier
            }
            view?.setupForTrack(track)
            return view
        }
        if let track = item as? iTunesTrack {
            var view = outlineView.makeViewWithIdentifier(OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier, owner: nil) as? AdvancedTableCellView
            if view == nil {
                view = AdvancedTableCellView()
                view?.identifier = OutlineViewConstants.ViewIdentifiers.simpleTableCellViewIdentifier
            }
            view?.style = .Simple
            view?.textField?.font = NSFont.systemFontOfSize(0)
            view?.textField?.textColor = NSColor.textColor()
            view?.textField?.stringValue = track.name
            return view
        }
        return nil
    }

    internal func outlineView(outlineView: NSOutlineView, writeItems items: [AnyObject], toPasteboard pasteboard: NSPasteboard) -> Bool {
        var rows = [Int]()
        var containsValidItems = false
        for item in items {
            let row = outlineView.rowForItem(item)
            rows.append(row)
            if sectionOfRow(row) != .SearchResults {
                containsValidItems = true
            }
        }
        if !containsValidItems {
            return false
        }
        let data = NSKeyedArchiver.archivedDataWithRootObject(rows)
        pasteboard.declareTypes([OutlineViewConstants.pasteboardType], owner: nil)
        pasteboard.setData(data, forType: OutlineViewConstants.pasteboardType)
        return true
    }
    
    internal func outlineView(outlineView: NSOutlineView, validateDrop info: NSDraggingInfo, proposedItem item: AnyObject?, proposedChildIndex index: Int) -> NSDragOperation {
        let firstUnsortedRow = outlineViewContents.count - (unsortedTracks.isEmpty ? 0 : unsortedTracks.count+1)
        // Drop in the 'unsorted' section
        if item == nil && index >= firstUnsortedRow || item === OutlineViewConstants.Items.unsortedTracksHeaderItem {
            outlineView.setDropItem(nil, dropChildIndex: outlineViewContents.count)
            return .Every
        }
        // Drop on iTunesTrack item or between items
        if index != NSOutlineViewDropOnItemIndex || item is iTunesTrack {
            return .None
        }
        // Drop on header row
        if item != nil && self.outlineView(outlineView, isGroupItem: item!) {
            return .None
        }
        // Drop in 'search results' section
        let row = outlineView.rowForItem(item)
        if sectionOfRow(row) == .SearchResults {
            return .None
        }
        if let album = item as? Album where albumCollection.isAlbumLoading(album) || albumCollection.errorForAlbum(album) != nil {
            return .None
        }
        return .Every
    }
    
    internal func outlineView(outlineView: NSOutlineView, acceptDrop info: NSDraggingInfo, item: AnyObject?, childIndex index: Int) -> Bool {
        guard let data = info.draggingPasteboard().dataForType(OutlineViewConstants.pasteboardType), draggedRows = NSKeyedUnarchiver.unarchiveObjectWithData(data) as? [Int] else {
            return false
        }
        // Get the dragged tracks and remove them from their previous location
        var draggedTracks = Set<iTunesTrack>()
        for row in draggedRows {
            if sectionOfRow(row) != .SearchResults {
                let item = outlineView.itemAtRow(row)
                if let album = item as? Album {
                    for track in album.tracks {
                        draggedTracks.unionInPlace(track.associatedTracks)
                        track.associatedTracks.removeAll()
                    }
                } else if let track = item as? Track {
                    draggedTracks.unionInPlace(track.associatedTracks)
                    track.associatedTracks.removeAll()
                } else if let track = item as? iTunesTrack {
                    draggedTracks.insert(track)
                    if let parentTrack = parentForTrack(track) {
                        parentTrack.associatedTracks.removeElement(track)
                    } else {
                        unsortedTracks.removeElement(track)
                    }
                }
            }
        }
        
        // Add the dragged tracks to the new target
        if let targetTrack = item as? Track {
            targetTrack.associatedTracks.appendContentsOf(draggedTracks)
        } else if let targetAlbum = item as? Album {
            for draggedTrack in draggedTracks {
                var inserted = false
                for track in targetAlbum.tracks {
                    if (draggedTrack.discNumber == track.discNumber || draggedTrack.discNumber == 0) && draggedTrack.trackNumber == track.trackNumber {
                        track.associatedTracks.append(draggedTrack)
                        inserted = true
                        break
                    }
                }
                if !inserted {
                    unsortedTracks.append(draggedTrack)
                }
            }
        } else {
            unsortedTracks.appendContentsOf(draggedTracks)
        }
        outlineView.reloadData()
        return true
    }
    
}

