//
// © 2018-2020 Qualcomm Technologies, Inc. and/or its subsidiaries. All rights reserved.
//

import UIKit
import MediaPlayer
import CocoaLumberjack

extension String {
	func containsCaseInsensitive(_ other: String) -> Bool {
		return self.range(of: other, options: .caseInsensitive) != nil
	}
}

extension Notification.Name {
	static let LocalMusicLibraryCacheInvalidated = Notification.Name("LocalMusicLibraryCacheInvalidated")
}

// MARK: -
class LocalMusicLibraryCache {
	// Private Properties
	private var items = [LocalMusicLibraryCacheItem] ()
	private var _albums: [LocalMusicLibraryCacheCollection]? = nil
	private var albums: [LocalMusicLibraryCacheCollection] {
		if let albums = _albums {
			return albums
		} else {
			var albums = Dictionary<UInt64, LocalMusicLibraryCacheCollection> ()
			items.forEach { item in
				let albumID = item.albumID
				if let album = albums[albumID] {
					album.items.append(item)
				} else {
					let album = LocalMusicLibraryCacheCollection(id: albumID)
					albums[albumID] = album
					album.items.append(item)
				}
			}
			
			let albumsArray = albums.values.sorted { $0.items.first!.albumTitle <  $1.items.first!.albumTitle }
			albumsArray.forEach { $0.items.sort { $0.trackNumber < $1.trackNumber } }
			_albums = albumsArray
			return albumsArray
		}
	}
	
	private var _genres: [LocalMusicLibraryCacheCollection]? = nil
	private var genres: [LocalMusicLibraryCacheCollection] {
		if let genres = _genres {
			return genres
		} else {
			var genres = Dictionary<UInt64, LocalMusicLibraryCacheCollection> ()
			items.forEach { item in
				let genreID = item.genreID
				if let genre = genres[genreID] {
					genre.items.append(item)
				} else {
					let genre = LocalMusicLibraryCacheCollection(id: genreID)
					genres[genreID] = genre
					genre.items.append(item)
				}
			}
			
			let genresArray = genres.values.sorted { $0.items.first!.genre <  $1.items.first!.genre }
			genresArray.forEach { genre in
				let albums = self.albums(forItems: genre.items)
				genre.subCollections = albums
			}
			_genres = genresArray
			return genresArray
		}
	}
	
	private var _artists: [LocalMusicLibraryCacheCollection]? = nil
	private var artists: [LocalMusicLibraryCacheCollection] {
		if let artists = _artists {
			return artists
		} else {
			var artists = Dictionary<UInt64, LocalMusicLibraryCacheCollection> ()
			items.forEach { item in
				let artistID = item.artistID
				if let artist = artists[artistID] {
					artist.items.append(item)
				} else {
					let artist = LocalMusicLibraryCacheCollection(id: artistID)
					artists[artistID] = artist
					artist.items.append(item)
				}
			}
			
			let artistsArray = artists.values.sorted { $0.items.first!.artist <  $1.items.first!.artist }
			artistsArray.forEach { artist in
				let albums = self.albums(forItems: artist.items)
				artist.subCollections = albums
			}
			_artists = artistsArray
			return artistsArray
		}
	}
	
	// MARK: Init/Deinit
	init() {
		setUpItems()
		NotificationCenter.default.addObserver(self,
											   selector: #selector(libraryDidChange(note:)),
											   name: .MPMediaLibraryDidChange,
											   object: nil)
		MPMediaLibrary.default().beginGeneratingLibraryChangeNotifications()
	}
	
	deinit {
		NotificationCenter.default.removeObserver(self, name: .MPMediaLibraryDidChange, object: nil)
		MPMediaLibrary.default().endGeneratingLibraryChangeNotifications()
	}
}

// MARK: - Public Methods
extension LocalMusicLibraryCache {
	func allSongs(searchString: String?) -> [LocalMusicLibraryCacheItem] {
		if let str = searchString {
			return items.filter { $0.title.containsCaseInsensitive(str) }
		} else {
			return items
		}
	}
	
	func allSongs(albumID: UInt64) -> [LocalMusicLibraryCacheItem] {
		if let album = albums.filter({ $0.id == albumID }).first {
			return album.items
		} else {
			return []
		}
	}
	
	func song(id: UInt64) -> LocalMusicLibraryCacheItem? {
		let song = items.filter({ $0.id == id }).first
		return song
	}
	
	func allAlbums(searchString: String?) -> [LocalMusicLibraryCacheCollection] {
		if let str = searchString {
			return albums.filter { $0.items.first!.albumTitle.containsCaseInsensitive(str) }
		} else {
			return albums
		}
	}
	
	func allAlbums(artistID: UInt64) -> [LocalMusicLibraryCacheCollection] {
		return albums.filter { $0.items.first!.artistID == artistID }
	}
	
	func allAlbums(genreID: UInt64) -> [LocalMusicLibraryCacheCollection] {
		return albums.filter { $0.items.first!.genreID == genreID }
	}
	
	func allArtists(searchString: String?) -> [LocalMusicLibraryCacheCollection] {
		if let str = searchString {
			return artists.filter { $0.items.first!.artist.containsCaseInsensitive(str) }
		} else {
			return artists
		}
	}
	
	func allGenres(searchString: String?) -> [LocalMusicLibraryCacheCollection] {
		if let str = searchString {
			return genres.filter { $0.items.first!.genre.containsCaseInsensitive(str) }
		} else {
			return genres
		}
	}
}

// MARK: - Private Methods
extension LocalMusicLibraryCache {
	@objc private func libraryDidChange(note: Notification) {
		setUpItems()
		NotificationCenter.default.post(Notification(name: .LocalMusicLibraryCacheInvalidated))
	}
	
	private func setUpItems() {
		let songsQuery = MPMediaQuery.songs()
		var cachedSongs = [LocalMusicLibraryCacheItem] ()
		guard let songs = songsQuery.items else {
			items = cachedSongs
			return
		}
		
		cachedSongs = songs.map { return LocalMusicLibraryCacheItem(mediaItem: $0) }
		cachedSongs.sort { $0.title < $1.title }
		items = cachedSongs
		_artists = nil
		_albums = nil
		_genres = nil
	}
	
	private func albums(forItems: [LocalMusicLibraryCacheItem]) -> [LocalMusicLibraryCacheCollection] {
		var albums = Dictionary<UInt64, LocalMusicLibraryCacheCollection> ()
		forItems.forEach { item in
			let albumID = item.albumID
			if let album = albums[albumID] {
				album.items.append(item)
			} else {
				let album = LocalMusicLibraryCacheCollection(id: albumID)
				albums[albumID] = album
				album.items.append(item)
			}
		}
		
		let albumsArray = albums.values.sorted { $0.items.first!.albumTitle <  $1.items.first!.albumTitle }
		albumsArray.forEach { $0.items.sort { $0.trackNumber < $1.trackNumber } }
		return albumsArray
	}
}

