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

import UIKit
import CocoaUPnP
import CocoaLumberjack

// MARK: Media Server
struct UPnPMediaServer {
	var name: String { return mediaServer.friendlyName }
	fileprivate let mediaServer: UPPMediaServerDevice
	fileprivate init(mediaServer: UPPMediaServerDevice) {
		self.mediaServer = mediaServer
	}
}

// MARK: Equatable
extension UPnPMediaServer: Equatable {
	static func ==(lhs: UPnPMediaServer, rhs: UPnPMediaServer) -> Bool {
		return lhs.mediaServer.udn == rhs.mediaServer.udn
	}
}

// MARK: - Media Item
struct UPnPMediaItem {
	// MARK: Public Properties
	// Some properties are specific for songs only
	var name: String { return mediaItem.itemTitle }
	var objectID: String { return mediaItem.objectID }
	var isFolder: Bool { return mediaItem.isContainer }
	var isMusicTrack: Bool { return mediaItem.objectClass == "object.item.audioItem.musicTrack" }
	var numberOfChildren: Int { return Int(mediaItem.childCount ?? "0") ?? 0 }
	var artist: String? { return mediaItem.artist }
	var albumTitle: String? { return mediaItem.albumTitle }
	var genre: String? { return mediaItem.genre }
	var trackNumber: Int { return Int(mediaItem.trackNumber  ?? "0") ?? 0 }
	var durationInSeconds: Int { return mediaItem.durationInSeconds }
	var itemURL: String? { return mediaItem.firstPlayableResource()?.resourceURLString }
    var artworkURL: String? {
		// We try to get first PNG for transparency
		if let firstPNG = imageResources.first(where: { return $0.resourceURLString.hasSuffix(".png") }) {
			return firstPNG.resourceURLString
		} else {
			return imageResources.first?.resourceURLString
		}
	}

	// MARK: Fileprivate Properties
	fileprivate var imageResources: [UPPMediaItemResource] {
        if let resources = mediaItem.resources {
			let images = resources.filter { resource in
				return resource.protocolInfo.contains ("image")
			}
			return images
		} else {
			return []
		}
	}

    fileprivate let mediaItem: UPPMediaItem
	
	// MARK: Fileprivate Initializer
	fileprivate init(mediaItem: UPPMediaItem) {
		self.mediaItem = mediaItem
	}
}

// MARK: -
extension Notification.Name {
	static var uPnPMediaServersDidChange = Notification.Name("uPnPMediaServersDidChange")
}

// MARK: - UPnPManager
class UPnPManager: NSObject {
	// MARK: Public Properties
	static let sharedInstance = UPnPManager()
	var mediaServers: [UPnPMediaServer] {
		let ret: [UPnPMediaServer] = syncQueue.sync {
			return devices.map { return UPnPMediaServer(mediaServer: $0) }
				.sorted{ (first, second) -> Bool in
					return first.name < second.name
			}
		}
		return ret
	}
	
	// MARK: Private Properties
	private let syncQueue = DispatchQueue(label: "com.qualcomm.qti.upnpmanager.sync")
	private var devices = [UPPMediaServerDevice] ()
    private weak var scanTimer: Timer?
    private let scanTimerInterval: TimeInterval = 27 // Try to miss Iotivity device scans at the same time.
}

// MARK: - UPnPManager Public Methods
extension UPnPManager {
	static func isAllowedItem(_ item: UPPMediaItem) -> Bool {
		let type = item.objectClass
		let accepted = type == "object.item.audioItem.musicTrack" ||
			type.hasPrefix("object.container")
		if !accepted {
			DDLogVerbose("Rejected Type: \(type)")
		}
		return accepted
	}
	
	func folderContents(mediaServer: UPnPMediaServer,
						objectID: String?,
						completion: @escaping ([UPnPMediaItem]?, Error?) -> (),
						completionQueue: DispatchQueue) {
		
		let handler: UPPResponseBlock = { (resp, error)  in
			if let response = resp, let results = response["Result"] as? [Any] {
				let items: [UPnPMediaItem] = results.compactMap({ item in
					if let mediaItem = item as? UPPMediaItem,
						UPnPManager.isAllowedItem(mediaItem) {
						return UPnPMediaItem(mediaItem: mediaItem)
					} else {
						return nil
					}
				})
					
				let sorted = items.sorted(by: {(first, second) -> Bool in
					return first.name < second.name
				}).sorted(by: {(first, second) -> Bool in
					return first.trackNumber < second.trackNumber
				}).sorted(by: { $0.isFolder && !$1.isFolder })
				completionQueue.async {
					completion(sorted, nil)
				}
			} else {
				completionQueue.async {
					completion(nil, error)
				}
			}
		}
		
		mediaServer.mediaServer.contentDirectoryService().browse(withObjectID: objectID,
																 browseFlag: BrowseDirectChildren,
																 filter: nil,
																 startingIndex: nil,
																 requestedCount: nil,
																 sortCritera: nil,
																 completion: handler)
	}
	
	func start() {
		UPPDiscovery.sharedInstance().addBrowserObserver(self)

        let t = Timer(timeInterval: scanTimerInterval,
                      target: self,
                      selector: #selector(timerFired(_:)),
                      userInfo: nil,
                      repeats: true)
        RunLoop.current.add(t, forMode: RunLoop.Mode.common)
        scanTimer = t
        timerFired(t)
    }

    @objc func timerFired(_ : Timer) {
        UPPDiscovery.sharedInstance().startBrowsing(forServices: "ssdp:all")
    }
}

// MARK: - UPnPManager Private Methods
extension UPnPManager {
	private func dispatch(notification: Notification) {
		DispatchQueue.main.async {
			NotificationCenter.default.post(notification)
		}
	}
	
	private func dispatchMediaServersChangedNotification() {
		let note = Notification(name: .uPnPMediaServersDidChange, object: self, userInfo: nil)
		dispatch(notification: note)
	}
}

// MARK: - UPnPManager UPPDiscoveryDelegate Conformance
extension UPnPManager: UPPDiscoveryDelegate {
	func discovery(_ discovery: UPPDiscovery, didFind device: UPPBasicDevice) {
		guard let mediaServer = device as? UPPMediaServerDevice else {
			return
		}
		
		syncQueue.async { [weak self] in
			guard let self = self else { return }
			if self.devices.firstIndex(of: mediaServer) == nil {
				DDLogInfo("Found: \(mediaServer)")
				self.devices.append(mediaServer)
				self.dispatchMediaServersChangedNotification()
			}
		}
	}
	
	func discovery(_ discovery: UPPDiscovery, didRemove device: UPPBasicDevice) {
		guard let mediaServer = device as? UPPMediaServerDevice else {
			return
		}
		
		syncQueue.async { [weak self] in
			guard let self = self else { return }
			if let index = self.devices.firstIndex(of: mediaServer) {
				DDLogInfo("Removed: \(mediaServer)")
				self.devices.remove(at: index)
				self.dispatchMediaServersChangedNotification()
			}
		}
	}
}

