import PRESTOplay
import AVFoundation

public class BridgeSerializer {
    static func toVideoTracks(
        tracks: [PRESTOplay.VideoTrack],
        currentTrack: PRESTOplay.VideoTrack?,
        currentRendition: PRESTOplay.VideoRendition?
    )
        -> [[String: Any]]
    {
        // If there is one video track, then it's active.
        // AVPlayer doesn't share this information properly.
        tracks.map {
            [
                "id": $0.id,
                "active": tracks.count == 1 || $0.id == currentTrack?.id,
                "renditions": toVideoRenditions(
                    renditions: $0.renditions,
                    currentRendition: currentRendition
                ),
            ]
        }
    }

    public static func toVideoRenditions(
        renditions: [PRESTOplay.VideoRendition],
        currentRendition: PRESTOplay.VideoRendition?
    )
        -> [[String: Any]]
    {
        // AVPlayer returns empty renditions.
        renditions.map {
            [
                "id": "\($0.trackId)-\($0.id)",
                "active": $0.id == currentRendition?.id && $0.trackId == currentRendition?.trackId,
                "width": $0.width,
                "height": $0.height,
                "trickplay": $0.trickplay,
                "bitrate": $0.bitrate,
                "codec": $0.codec,
                "properties": $0.properties.map { ["key": $0.key,  "value": $0.value] },
                "roles": $0.roles.map { ["schemeId": $0.key, "value": $0.value] },
                "protected": $0.protected,
                "supported": $0.supported,
                "filtered": $0.filtered,
                "blocked": $0.blocked,
                "drmLicenseExpected": $0.drmLicenseExpected,
                "drmLicenseAvailable": $0.drmLicenseAvailable,
                "selectable": $0.selectable
            ]
        }
    }
    
    static func toVideoTracks(
        tracks: [AVAssetTrack]
    ) async throws -> [[String: Any]]
    {
        // If there is one video track, then it's active.
        // AVPlayer doesn't share this information properly.
        
        var results: [[String: Any]] = []
        
        for track in tracks {
            results.append([
                "id": track.trackID,
                "active": track.isEnabled,
                "renditions": try await toVideoRenditions(
                    renditions: tracks
                )
            ])
        }
        return results;
    }
    
    public static func toVideoRenditions(
        renditions: [AVAssetTrack]
    ) async throws -> [[String: Any]]  {
        
        var results: [[String: Any]] = []
        
        for track in renditions {
            let size = try await track.load(.naturalSize)
            results.append([
                "id": "\(track.trackID)",
                "active": track.isEnabled,
                "width": size.width,
                "height": size.height
            ])
        }
        
        return results
    }
    
    public static func toAudioTracks(
        tracks: [PRESTOplay.AudioTrack],
        currentTrack: PRESTOplay.AudioTrack?
    )
        -> [[String: Any]]
    {
        tracks.map {
            [
                "id": $0.id,
                "active": $0.id == currentTrack?.id,
                "label": $0.label,
                "language": $0.language,
                "descriptors": toDescriptors(track: $0)
            ]
        }
    }

    public static func toTextTracks(
        tracks: [PRESTOplay.TextTrack],
        currentTrack: PRESTOplay.TextTrack?
    )
        -> [[String: Any]]
    {
        tracks.map {
            [
                "id": $0.id,
                "active": $0.id == currentTrack?.id,
                "language": $0.language,
                "label": $0.label,
                "mimeType": toMimeType(textTrack: $0),
                "descriptors": toDescriptors(track: $0)
            ]
        }
    }
    
    public static func mediaOptionIdentifier(_ option: AVMediaSelectionOption) -> String {
        // Combine several properties to make it unique-ish and persistent
        let locale = option.locale?.identifier ?? "unknown"
        let name = option.displayName
        let type = option.mediaType.rawValue
        return "\(type)_\(locale)_\(name)"
    }
    
    public static func toAudioTracks(
        tracks: AVMediaSelectionGroup,
        currentTrack: AVMediaSelectionOption?
    ) async throws -> [[String: Any]]  {
        
        var results: [[String: Any]] = []
        
        for track in tracks.options {
            
/*
            track.metadata.forEach { item in
                if let key = item.commonKey?.rawValue, let value = item.value {
                    print("Track \(track.trackID): \(key) = \(value)")
                } else {
                    print("Track \(track.trackID): item = \(item)")
                }
            }
*/
            results.append([
                "id": mediaOptionIdentifier(track),
                "active": (currentTrack != nil) ? mediaOptionIdentifier(track) == mediaOptionIdentifier(currentTrack!) : false,
                "label": track.displayName,
                "language": track.extendedLanguageTag ?? "",
                //"descriptors": toDescriptors(track: track)
            ])
        }
        
        return results
    }
    
    public static func toTextTracks(
        tracks: AVMediaSelectionGroup,
        currentTrack: AVMediaSelectionOption?
    ) async throws -> [[String: Any]]  {
        
        var results: [[String: Any]] = []
        for track in tracks.options {
            // TODO: get mimeType
            results.append([
                "id": mediaOptionIdentifier(track),
                "active": (currentTrack != nil) ? mediaOptionIdentifier(track) == mediaOptionIdentifier(currentTrack!) : false,
                "language": track.extendedLanguageTag,
                //"mimeType": mediaSubType,
                //"descriptors": track.
            ])
        }
        
        return results
    }

    static func toBase64Encoded(
        data: Data?
    )
        -> String?
    {
        guard let data = data else {
            return nil
        }
        return data.base64EncodedString()
    }

    static func toPlayerState(_ playbackState: PlaybackState) -> String {
        switch playbackState {
        case .idle:
            return "Idle"
        case .opening:
            return "Opening"
        case .stopping:
            return "Stopping"
        case .error:
            return "Error"
        case .ready:
            return "Ready"
        case .seeking:
            return "Seeking"
        case .buffering:
            return "Buffering"
        case .play:
            return "Play"
        case .playing:
            return "Playing"
        case .paused:
            return "Paused"
        case .pausing:
            return "Pausing"
        case .ended:
            return "Ended"
        default:
            fatalError("Unexpected playback state")
        }
    }

    public static func toPlaybackStats(stats: Stats, bufferFullnessSec: Float?) -> [String: Any] {
        var results: [String: Any] = [:]

        // basic metrics unified across Android and iOS

        results["droppedFrames"] = stats.numberOfDroppedVideoFrames > 0
            ? stats.numberOfDroppedVideoFrames
            : 0

        if stats.indicatedBitrate > 0 {
            results["streamBandwidth"] = stats.indicatedBitrate
        }

        if stats.observedBitrate > 0 {
            results["estimatedBandwidth"] = stats.observedBitrate
        } else if stats.numberOfBytesTransferred > 0, stats.transferDuration > 0 {
            let bits = Double(stats.numberOfBytesTransferred) * 8

            // Often, transferDuration is below 1 second and remains constant.
            // Once quality stabilizes, updates stop. This appears to be platform behavior.
            let seconds = stats.transferDuration
            results["estimatedBandwidth"] = bits / seconds
        }

        // extra metrics specific to iOS

        if stats.transferDuration >= 0 {
            results["transferDurationSec"] = stats.transferDuration
        }
        if stats.numberOfBytesTransferred >= 0 {
            results["numberOfBytesTransferred"] = stats.numberOfBytesTransferred
        }
        if stats.numberOfMediaRequests >= 0 {
            results["numberOfMediaRequests"] = stats.numberOfMediaRequests
        }
        if let timeIntervalSince1970 = stats.playbackStartDate?.timeIntervalSince1970 {
            results["playbackStartTimestamp"] =
                Int64(timeIntervalSince1970 * 1000) // milliseconds since epoch
        }
        if stats.startupTime >= 0 {
            results["startupTimeSec"] = stats.startupTime
        }
        if stats.durationWatched >= 0 {
            results["durationWatchedSec"] = stats.durationWatched
        }
        if stats.numberOfStalls >= 0 {
            results["numberOfStalls"] = stats.numberOfStalls
        }
        if stats.segmentsDownloadedDuration >= 0 {
            results["segmentsDownloadedDurationSec"] = stats.segmentsDownloadedDuration
        }
        if stats.observedBitrateStandardDeviation >= 0 {
            results["observedBitrateStandardDeviation"] = stats.observedBitrateStandardDeviation
        }
        if stats.observedBitrate >= 0 {
            results["observedBitrate"] = stats.observedBitrate
        }
        if stats.averageAudioBitrate >= 0 {
            results["averageAudioBitrate"] = stats.averageAudioBitrate
        }
        if stats.averageVideoBitrate >= 0 {
            results["averageVideoBitrate"] = stats.averageVideoBitrate
        }
        if stats.indicatedAverageBitrate >= 0 {
            results["indicatedAverageBitrate"] = stats.indicatedAverageBitrate
        }
        if stats.indicatedBitrate >= 0 {
            results["indicatedBitrate"] = stats.indicatedBitrate
        }
        
        if let bufferTime = bufferFullnessSec {
            if bufferTime > 0 {
                let bufferFullnessMsec = Int64(bufferTime * 1000)
                results["bufferedTimeMsec"] = bufferFullnessMsec
            }
        }

        return results
    }

    public static func toDescriptors(track: Track) -> [String: Any] {
        guard let characteristics = track.properties["characteristics"] else {
            return [:]
        }
        
        return ["hlsCharacteristics": ["value": characteristics]]
    }

    public static func toMimeType(textTrack: TextTrack) -> String {
        switch textTrack.format {
        case .ttml: "application/ttml+xml"
        case .web_vtt: "text/vtt"
        default: "text/plain" // srt and others
        }
    }
    
    public static func toMillisecondsOrNil(seconds: Double?) -> Int64? {
        if let s = seconds {
            return toMilliseconds(seconds: s)
        } else {
            return nil
        }
    }
    
    public static func toMilliseconds(seconds: Double) -> Int64 {
        if !seconds.isFinite {
            return 0
        }
        return Int64(seconds * 1000)
    }
}
