//
//  LocalLibraryItem.swift
//  App
//
//  Created by Ron Heft on 8/16/22.
//

import Foundation
import RealmSwift

class LocalLibraryItem: Object, Codable {
    @Persisted(primaryKey: true) var id: String = "local_\(UUID().uuidString)"
    @Persisted var basePath: String = ""
    @Persisted var _contentUrl: String?
    @Persisted var isInvalid: Bool = false
    @Persisted var mediaType: String = ""
    @Persisted var media: MediaType?
    @Persisted var localFiles = List<LocalFile>()
    @Persisted var _coverContentUrl: String?
    @Persisted var isLocal: Bool = true
    @Persisted var serverConnectionConfigId: String?
    @Persisted var serverAddress: String?
    @Persisted var serverUserId: String?
    @Persisted(indexed: true) var libraryItemId: String?

    var contentUrl: String? {
        if let path = _contentUrl {
            return AbsDownloader.itemDownloadFolder(path: path)!.absoluteString
        } else {
            return nil
        }
    }
    
    var contentDirectory: URL? {
        if let path = _contentUrl {
            return AbsDownloader.itemDownloadFolder(path: path)
        } else {
            return nil
        }
    }
    
    var coverUrl: URL? {
        if let path = self._coverContentUrl {
            return AbsDownloader.itemDownloadFolder(path: path)
        } else {
            return nil
        }
    }
    
    var coverContentUrl: String? {
        return self.coverUrl?.absoluteString
    }
    
    var isBook: Bool { self.mediaType == "book" }
    var isPodcast: Bool { self.mediaType == "podcast" }
    
    private enum CodingKeys : String, CodingKey {
        case id, basePath, contentUrl, isInvalid, mediaType, media, localFiles, coverContentUrl, isLocal, serverConnectionConfigId, serverAddress, serverUserId, libraryItemId
    }
    
    override init() {
        super.init()
    }
    
    required init(from decoder: Decoder) throws {
        super.init()

        let values = try decoder.container(keyedBy: CodingKeys.self)
        id = try values.decode(String.self, forKey: .id)
        basePath = try values.decode(String.self, forKey: .basePath)
        isInvalid = try values.decode(Bool.self, forKey: .isInvalid)
        mediaType = try values.decode(String.self, forKey: .mediaType)
        media = try? values.decode(MediaType.self, forKey: .media)
        if let files = try? values.decode([LocalFile].self, forKey: .localFiles) {
            localFiles.append(objectsIn: files)
        }
        isLocal = try values.decode(Bool.self, forKey: .isLocal)
        serverConnectionConfigId = try? values.decode(String.self, forKey: .serverConnectionConfigId)
        serverAddress = try? values.decode(String.self, forKey: .serverAddress)
        serverUserId = try? values.decode(String.self, forKey: .serverUserId)
        libraryItemId = try? values.decode(String.self, forKey: .libraryItemId)
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(id, forKey: .id)
        try container.encode(basePath, forKey: .basePath)
        try container.encode(contentUrl, forKey: .contentUrl)
        try container.encode(isInvalid, forKey: .isInvalid)
        try container.encode(mediaType, forKey: .mediaType)
        try container.encode(media, forKey: .media)
        try container.encode(Array(localFiles), forKey: .localFiles)
        try container.encode(coverContentUrl, forKey: .coverContentUrl)
        try container.encode(isLocal, forKey: .isLocal)
        try container.encode(serverConnectionConfigId, forKey: .serverConnectionConfigId)
        try container.encode(serverAddress, forKey: .serverAddress)
        try container.encode(serverUserId, forKey: .serverUserId)
        try container.encode(libraryItemId, forKey: .libraryItemId)
    }
}

extension LocalLibraryItem {
    convenience init(_ item: LibraryItem, localUrl: String, server: ServerConnectionConfig, files: [LocalFile], coverPath: String?) {
        self.init()
        
        self._contentUrl = localUrl
        self.mediaType = item.mediaType
        self.localFiles.append(objectsIn: files)
        self._coverContentUrl = coverPath
        self.libraryItemId = item.id
        self.serverConnectionConfigId = server.id
        self.serverAddress = server.address
        self.serverUserId = server.userId
        
        // Link the audio tracks and files
        linkLocalFiles(self.localFiles, fromMedia: item.media)
    }
    
    func addFiles(_ files: [LocalFile], item: LibraryItem) throws {
        guard self.isPodcast else { throw LibraryItemDownloadError.podcastOnlySupported }
        self.localFiles.append(objectsIn: files.filter({ $0.isAudioFile() }))
        linkLocalFiles(self.localFiles, fromMedia: item.media)
    }
    
    private func linkLocalFiles(_ files: List<LocalFile>, fromMedia: MediaType?) {
        guard let fromMedia = MediaType.detachCopy(of: fromMedia) else { return }
        let fileMap = files.map { ($0.filename ?? "", $0.id) }
        let fileIdByFilename = Dictionary(fileMap, uniquingKeysWith: { (_, last) in last })
        if ( self.isBook ) {
            for i in fromMedia.tracks.indices {
                _ = fromMedia.tracks[i].setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: i)
            }
        } else if ( self.isPodcast ) {
            let episodes = List<PodcastEpisode>()
            for episode in fromMedia.episodes {
                // Filter out episodes not downloaded
                let episodeIsDownloaded = episode.audioTrack?.setLocalInfo(filenameIdMap: fileIdByFilename, serverIndex: 0) ?? false
                if episodeIsDownloaded {
                    episodes.append(episode)
                }
            }
            fromMedia.episodes = episodes
        }
        self.media = fromMedia
    }
    
    func getDuration() -> Double {
        var total = 0.0
        self.media?.tracks.enumerated().forEach { _, track in total += track.duration }
        return total
    }
    
    func getPodcastEpisode(episodeId: String?) -> PodcastEpisode? {
        guard self.isPodcast else { return nil }
        guard let episodes = self.media?.episodes else { return nil }
        return episodes.first(where: { $0.id == episodeId })
    }
    
    func getPlaybackSession(episode: PodcastEpisode?) -> PlaybackSession {
        let localEpisodeId = episode?.id
        let sessionId = "play_local_\(UUID().uuidString)"
        
        // Get current progress from local media
        let mediaProgressId = (localEpisodeId != nil) ? "\(self.id)-\(localEpisodeId!)" : self.id
        let mediaProgress = Database.shared.getLocalMediaProgress(localMediaProgressId: mediaProgressId)
        
        let mediaMetadata = Metadata.detachCopy(of: self.media?.metadata)
        let chapters = List<Chapter>()
        self.media?.chapters.forEach { chapter in chapters.append(Chapter.detachCopy(of: chapter)!) }
        let authorName = mediaMetadata?.authorDisplayName
        
        var duration = getDuration()
        var displayTitle = mediaMetadata?.title

        let audioTracks = List<AudioTrack>()
        if let episode = episode, let track = episode.audioTrack {
            audioTracks.append(AudioTrack.detachCopy(of: track)!)
            duration = track.duration
            displayTitle = episode.title
        } else if let tracks = self.media?.tracks {
            tracks.forEach { t in audioTracks.append(AudioTrack.detachCopy(of: t)!) }
        }
        
        let dateNow = Date().timeIntervalSince1970 * 1000
        return PlaybackSession(
            id: sessionId,
            userId: self.serverUserId,
            libraryItemId: self.libraryItemId,
            episodeId: episode?.serverEpisodeId,
            mediaType: self.mediaType,
            mediaMetadata: mediaMetadata,
            chapters: chapters,
            displayTitle: displayTitle,
            displayAuthor: authorName,
            coverPath: self.coverContentUrl,
            duration: duration,
            playMethod: PlayMethod.local.rawValue,
            startedAt: dateNow,
            updatedAt: dateNow,
            timeListening: 0.0,
            audioTracks: audioTracks,
            currentTime: mediaProgress?.currentTime ?? 0.0,
            libraryItem: nil,
            localLibraryItem: self,
            serverConnectionConfigId: self.serverConnectionConfigId,
            serverAddress: self.serverAddress
        )
    }
    
    func delete() throws {
        try self.realm?.write {
            self.realm?.delete(self.localFiles)
            self.realm?.delete(self)
        }
    }
}
