import UIKit
import BrightcovePlayerSDK
import Network

struct VideoDownload {
    let video: BCOVVideo
    let paramaters: [String:Any]
}

@available(iOS 12.0, *)
class DownloadManager: NSObject {
    private var videoPreloadQueue: [VideoDownload] = []
    private var videoDownloadQueue: [VideoDownload] = []
    private var downloadInProgress = false
    
    private var videoToDownload: BCOVVideo?
    weak var delegate: ReloadDelegate?
    static var shared = DownloadManager()
    private var setup: BrightcoveSetup?

    override init() {
        super.init()
        BCOVOfflineVideoManager.initializeOfflineVideoManager(
            with: self,
            options: [
                kBCOVOfflineVideoManagerAllowsCellularDownloadKey: true,
                kBCOVOfflineVideoManagerAllowsCellularPlaybackKey: true,
                kBCOVOfflineVideoManagerAllowsCellularAnalyticsKey: true
            ])
    }
    
    func setSetup(setup: BrightcoveSetup) -> Void { self.setup = setup }
    
    func deleteAllDownloadedMedias() throws -> Void {
        var state: [String: [String: String]] = [:]
        
        for offlineVideoStatus in BCOVOfflineVideoManager.shared()!.offlineVideoStatus() {
            let token = offlineVideoStatus.offlineVideoToken!
            let id = self.getMediaIdFromToken(token: token)!
            
            BCOVOfflineVideoManager.shared()?.deleteOfflineVideo(token)
            
            state[id] = ["status": DownloadState.DELETED.rawValue]
        }
        
        self.downloadEvent(state: state)
    }
    
    func deleteDownloadedMedia(fileId: String?) throws -> Void {
        guard let id = fileId else {
            throw PluginError.MISSING_FILEID
        }
        let tokenToDelete = self.getTokenFromMediaId(fileId: id)
        if(tokenToDelete == "") {
            throw PluginError.DOWNLOADED_FILE_NOT_FOUND
        }
        
        BCOVOfflineVideoManager.shared()?.deleteOfflineVideo(tokenToDelete)
        self.downloadEvent(state: [id:["status": DownloadState.DELETED.rawValue]])
    }
    
    func getDownloadedMediasState() throws -> [[String: Any]] {
        var videosStatus: [[String: Any]] = []
        
        for offlineVideoStatus in BCOVOfflineVideoManager.shared()!.offlineVideoStatus() {
            let token = offlineVideoStatus.offlineVideoToken!;
            let video = BCOVOfflineVideoManager.shared()!.videoObject(fromOfflineVideoToken: token)!
            let size = getMediaSize(fileId: video.properties[kBCOVVideoPropertyKeyId]! as! String)
            
            var videoStatus = [
                "id": video.properties[kBCOVVideoPropertyKeyId]!,
                "status": try self.getDownloadState(state : offlineVideoStatus.downloadState).rawValue
            ]
            
            if(size != -1) {
                    videoStatus["estimatedSize"] = size
            }
            
            videosStatus.append(videoStatus)
        }
        
        return videosStatus
    }
    
    private func getMediaSize(fileId: String) -> Int {
        for offlineVideoStatus in BCOVOfflineVideoManager.shared()!.offlineVideoStatus() {
            let token = offlineVideoStatus.offlineVideoToken!;
            let video = BCOVOfflineVideoManager.shared()?.videoObject(fromOfflineVideoToken: token)!
            
            let downloadedFileId = video!.properties[kBCOVVideoPropertyKeyId]! as! String
            if (downloadedFileId == fileId) {
                let videoFilePath = video!.properties[kBCOVOfflineVideoFilePathPropertyKey]

                // Get local file size
                if(videoFilePath != nil && self.isVideoDownloaded(video!)) {
//                    return Int(FileHelper.directorySize(folderPath: videoFilePath! as! String))
                } else {
                    // Video not fully downloaded
                    return -1
                }
            }
        }
        // Video not founded
        return -1
    }
    
    private func getMediaIdFromToken(token: String) -> String? {
        BCOVOfflineVideoManager.shared()?.videoObject(fromOfflineVideoToken: token)!.properties[kBCOVVideoPropertyKeyId]! as! String?
    }
    
    private func getTokenFromMediaId(fileId: String) -> String {
        for offlineVideoStatus in BCOVOfflineVideoManager.shared()!.offlineVideoStatus() {
            let token = offlineVideoStatus.offlineVideoToken!
            let downloadedFileId = getMediaIdFromToken(token: token)
            if (downloadedFileId! == fileId) {
                return token
            }
        }
        return ""
    }

    func isMediaAvailableLocally(fileId: String?) throws -> Bool {
        guard let id = fileId else {
            throw PluginError.MISSING_FILEID
        }
        let token = self.getTokenFromMediaId(fileId: id)
        
        if(token == "") {
            return false
        } else {
            return self.isVideoDownloaded((BCOVOfflineVideoManager.shared()?.videoObject(fromOfflineVideoToken: token))!)
        }
    }
    
    func download(fileId : String?) throws {
        if(fileId == nil || fileId! == "") {
            throw PluginError.MISSING_FILEID
        }
        
        let group = DispatchGroup()
        group.enter();
        var findVideoError: Error?

        self.setup!.playbackService.findVideo(withVideoID: fileId!, parameters: nil) { (video: BCOVVideo?, jsonResponse: [AnyHashable: Any]?, error: Error?) -> Void in
            findVideoError = error
            self.videoToDownload = video
            group.leave()
        }
        
        group.wait()
        
        if findVideoError != nil {
            throw findVideoError!
        }
         
        if(self.videoToDownload == nil || !self.videoToDownload!.canBeDownloaded) {
            throw PluginError.VIDEO_CANT_BE_DOWNLOADED
        }

        if(!self.videoAlreadyProcessing(self.videoToDownload!)) {
            self.downloadEvent(state: [fileId!:["status": DownloadState.REQUESTED.rawValue]])
            self.doDownload(forVideo: self.videoToDownload!)
        }
    }
    
    func doDownload(forVideo video: BCOVVideo) {
        if videoAlreadyProcessing(video) {
            return
        }
        
        let downloadParamaters = DownloadManager.generateDownloadParameters()
        let videoDownload = VideoDownload(video: video, paramaters: downloadParamaters)
        videoPreloadQueue.append(videoDownload)
        runPreloadVideoQueue()
    }

    private func languagesArrayForAlternativeRenditions(attributesDictArray: [[AnyHashable:Any]]?) -> [String] {
        // We want to download all subtitle/audio tracks
        guard let attributesDictArray = attributesDictArray else {
            return []
        }

        // Collect all the available subtitle languages in a set to avoid duplicates
        var languageSet = Set<String>()
        for attributeDict in attributesDictArray {
            if let typeString = attributeDict["TYPE"] as? String, let langString = attributeDict["LANGUAGE"] as? String {
                if typeString == "SUBTITLES" {
                    languageSet.insert(langString)
                }
            }
        }
        
        let languagesArray = Array(languageSet)
        // For debugging: display the languages we found
        var languagesString = String()
        var first = true
        for languageString in languagesArray {
            // Add comma before each entry after the first
            if first {
                first = false
            } else {
                languagesString = languagesString + ", "
            }
            
            languagesString = languagesString + languageString
        }
        
        print("Languages to download: \(languagesString)")
        
        return languagesArray
    }
    
    func isVideoDownloaded(_ video: BCOVVideo) -> Bool {
        guard let offlineVideoTokens = BCOVOfflineVideoManager.shared()?.offlineVideoTokens else {
            return false
        }
        
        for offlineVideoToken in offlineVideoTokens {
            guard let testVideo = BCOVOfflineVideoManager.shared()?.videoObject(fromOfflineVideoToken: offlineVideoToken) else {
                continue
            }
            
            if testVideo.matches(offlineVideo: video) {
                _ = localizedNameForLocale(video, nil) ?? ""
                
                if let downloadStatus = BCOVOfflineVideoManager.shared()?.offlineVideoStatus(forToken: offlineVideoToken) {
                    return downloadStatus.downloadState == .stateCompleted
                }
            }
        }
        
        return false
    }
        
    private func videoAlreadyProcessing(_ video: BCOVVideo) -> Bool {
        // First check to see if the video is in a preload queue
        // videoPreloadQueue is an array of NSDictionary objects,
        // with a BCOVVideo under each "video" key.
        
        for videoDict in videoPreloadQueue {
            if videoDict.video.matches(offlineVideo: video) {
                return true
            }
        }
        
        // First check to see if the video is in a download queue
        // videoDownloadQueue is an array of BCOVVideo objects
        for videoDict in videoDownloadQueue {
            if videoDict.video.matches(offlineVideo: video) {
                return true
            }
        }
        
        // Next check to see if the video has already been downloaded
        // or is in the process of downloading
        guard let offlineVideoTokens = BCOVOfflineVideoManager.shared()?.offlineVideoTokens else {
            return false
        }
        
        for offlineVideoToken in offlineVideoTokens {
            guard let testVideo = BCOVOfflineVideoManager.shared()?.videoObject(fromOfflineVideoToken: offlineVideoToken) else {
                continue
            }
            
            if testVideo.matches(offlineVideo: video) {
                if let downloadStatus = BCOVOfflineVideoManager.shared()?.offlineVideoStatus(forToken: offlineVideoToken) {
                    if downloadStatus.downloadState == .stateError {
                        BCOVOfflineVideoManager.shared()?.deleteOfflineVideo(offlineVideoToken)
                        return false
                    }
                }
                return true
            }
        }
        return false
    }

    private func runPreloadVideoQueue() {
        guard let videoDownload = videoPreloadQueue.first else {
            downloadVideoFromQueue()
            return
        }
        
        let video = videoDownload.video
        
        if let indexOfVideo = videoPreloadQueue.firstIndex(where: { $0.video.matches(offlineVideo: video) }) {
            videoPreloadQueue.remove(at: indexOfVideo)
        }
        
        // Preloading only applies to FairPlay-protected videos.
        // If there's no FairPlay involved, the video is moved on
        // to the video download queue.
        
        if !video.usesFairPlay {
            if let videoName = localizedNameForLocale(video, nil) {
                print("Video \"\(videoName)\" does not use FairPlay; preloading not necessary")
            }
            videoDownloadQueue.append(videoDownload)
            
            delegate?.reloadRow(forVideo: video)
            runPreloadVideoQueue()
        } else {
            BCOVOfflineVideoManager.shared()?.preloadFairPlayLicense(video, parameters: videoDownload.paramaters, completion: { [weak self] (offlineVideoToken: String?, error: Error?) in
                DispatchQueue.main.async {
                    if let error = error {
                        // Report any errors
                        self!.downloadEvent(state: [self!.getMediaIdFromToken(token: offlineVideoToken!)!: ["status": DownloadState.FAILED.rawValue, "reason": error.localizedDescription]])

                    } else {
                        if let offlineVideoToken = offlineVideoToken {
                            print("Preloaded \(offlineVideoToken)")
                        }
                        self?.videoDownloadQueue.append(videoDownload)
                        self?.delegate?.reloadRow(forVideo: video)
                    }
                    
                    self?.runPreloadVideoQueue()
                }
            })
        }
    }
    
    private func getDownloadState(state: BCOVOfflineVideoDownloadState) throws -> DownloadState {
        print("Download state : Raw value state \(state.rawValue)")
        
        switch state.rawValue {
        case 0:
            return DownloadState.REQUESTED
        case 1:
            return DownloadState.IN_PROGRESS
        case 2:
            return DownloadState.PAUSED
        case 3:
            return DownloadState.CANCELED
        case 4:
            return DownloadState.COMPLETED
        case 5:
            return DownloadState.FAILED
        default:
            throw PluginError.DOWNLOAD_STATUS_NOT_DETERMINED
        }
    }
    
    private func downloadVideoFromQueue() {
        guard let videoDownload = videoDownloadQueue.first else {
            return
        }
        
        let video = videoDownload.video
        
        if let indexOfVideo = videoDownloadQueue.firstIndex(where: { $0.video.matches(offlineVideo: video) }) {
            videoDownloadQueue.remove(at: indexOfVideo)
        }
        
        downloadInProgress = true
        
        // Display all available bitrates
        BCOVOfflineVideoManager.shared()?.variantBitrates(for: video, completion: { (bitrates: [NSNumber]?, error: Error?) in
            if let name = localizedNameForLocale(video, nil) {
                print("Variant Bitrates for video: \(name)")
            }
            
            if let bitrates = bitrates {
                for bitrate in bitrates {
                    print("\(bitrate.intValue)")
                }
            }
            
        })
    
        var avURLAsset: AVURLAsset?
        do {
            avURLAsset = try BCOVOfflineVideoManager.shared()?.urlAsset(for: video)
        } catch {}
        
        // If mediaSelections is `nil` the SDK will default to the AVURLAsset's `preferredMediaSelection`
        var mediaSelections = [AVMediaSelection]()
        
        if let avURLAsset = avURLAsset {
            mediaSelections = avURLAsset.allMediaSelections
            
            if let legibleMediaSelectionGroup = avURLAsset.mediaSelectionGroup(forMediaCharacteristic: .legible), let audibleMediaSelectionGroup = avURLAsset.mediaSelectionGroup(forMediaCharacteristic: .audible) {
                
                var counter = 0
                for selection in mediaSelections {
                    let legibleMediaSelectionOption = selection.selectedMediaOption(in: legibleMediaSelectionGroup)
                    let audibleMediaSelectionOption = selection.selectedMediaOption(in: audibleMediaSelectionGroup)
                    
                    let legibleName = legibleMediaSelectionOption?.displayName ?? "nil"
                    let audibleName = audibleMediaSelectionOption?.displayName ?? "nil"
                    
                    print("AVMediaSelection option \(counter) | legible display name: \(legibleName)")
                    print("AVMediaSelection option \(counter) | audible display name: \(audibleName)")
                    counter += 1
                }
                
            }
        }
        
        BCOVOfflineVideoManager.shared()?.requestVideoDownload(video, mediaSelections: mediaSelections, parameters: videoDownload.paramaters, completion: { [weak self] (offlineVideoToken: String?, error: Error?) in
            
            DispatchQueue.main.async {
                if let error = error, let self = self {
                    self.downloadInProgress = false
                    
                    // Report any errors
                    if let offlineVideoToken = offlineVideoToken, let offlineVideo = BCOVOfflineVideoManager.shared()?.videoObject(fromOfflineVideoToken: offlineVideoToken), let _ = localizedNameForLocale(offlineVideo, nil) {
                        self.downloadEvent(state : [self.getMediaIdFromToken(token: offlineVideoToken)!: ["status": DownloadState.FAILED.rawValue, "reason": error.localizedDescription]])
                    }
                }
            }
        })
    }
}

// MARK: - Class Methods
@available(iOS 12.0, *)
extension DownloadManager {
    class func generateDownloadParameters() -> [String:Any] {
        var downloadParameters : [String: Int] = [:]
        downloadParameters[kBCOVOfflineVideoManagerRequestedBitrateKey] = 0
        
        return downloadParameters
    }
}

// MARK: - BCOVOfflineVideoManagerDelegate
@available(iOS 12.0, *)
extension DownloadManager: BCOVOfflineVideoManagerDelegate {
    
    func didCreateSharedBackgroundSesssionConfiguration(_ backgroundSessionConfiguration: URLSessionConfiguration!) {
        // Helps prevent downloads from appearing to sometimes stall
        backgroundSessionConfiguration.isDiscretionary = false
    }
        
    // Download in progress
    func offlineVideoToken(_ offlineVideoToken: String!, aggregateDownloadTask: AVAggregateAssetDownloadTask!, didProgressTo progressPercent: TimeInterval, for mediaSelection: AVMediaSelection!) {
        if let offlineVideoToken = offlineVideoToken {
            self.downloadEvent(state: [self.getMediaIdFromToken(token: offlineVideoToken)!:["status": DownloadState.IN_PROGRESS.rawValue,"progress": String(progressPercent)]])
        }
    }
    
    // Download complete
    func offlineVideoToken(_ offlineVideoToken: String?, didFinishDownloadWithError error: Error?) {
        if let error = error {
            self.downloadEvent(state : [self.getMediaIdFromToken(token: offlineVideoToken!)!: ["status": DownloadState.FAILED.rawValue, "reason": error.localizedDescription]])
        } else {
            self.downloadEvent(state : [self.getMediaIdFromToken(token: offlineVideoToken!)! :
                                ["status": DownloadState.COMPLETED.rawValue,
                                 "estimatedSize": self.getMediaSize(fileId: self.getMediaIdFromToken(token: offlineVideoToken!)!)]])
        }
        
        downloadInProgress = false
    }

    // download event fired to the plugin
    private func downloadEvent(state: [String :[String: Any]]) {
        NotificationCenter.default.post(
            name: Notification.Name("downloadStateChange"),
            object: nil,
            userInfo: state
        )
    }
}

private enum DownloadState: String {
    case REQUESTED = "REQUESTED"
    case IN_PROGRESS = "IN_PROGRESS"
    case PAUSED = "PAUSED"
    case CANCELED = "CANCELED"
    case COMPLETED = "COMPLETED"
    case DELETED = "DELETED"
    case FAILED = "FAILED"
}

