//
//  ApplePlayerProxy.swift
//  Pods
//
//  Created by Asti Manuka on 27/11/23.
//

import Foundation

import AVKit
import Foundation
import Combine

import PRESTOplay

class ApplePlayerProxy: NSObject, PlayerProtocol {
    public let id: String

    // MARK: Lifecycle

    init(playerId: String, playerEventEmitter: PlayerEventEmitter) {
        id = playerId
        eventEmitter = playerEventEmitter
        volume = 1;
        super.init()
        
        everyPlugin { plugin in
            plugin.onPlayerCreated(player: self, playerEventEmitter: self.eventEmitter!)
        }
    }

    deinit {
        destroy()
    }

    // MARK: Public

    public func destroy() {
        lock.lock()
        defer {
            lock.unlock()
        }

        if playerView != nil {
            playerView?.pause()
            playerView?.close()
            playerView = nil
        }
        
        unregisterListeners()

        pendingSuperView = nil
        pendingSize = nil

        eventEmitter = nil
    }

    public func open(
        jsonPlayerConfiguration: [String: Any],
        completion: @escaping () -> Void
    ) throws {
        lock.lock()
        defer {
            lock.unlock()
        }

        let playerConfiguration = try BridgeDeserializer.toPlayerConfiguration(
            jsonPlayerConfiguration
        )
    
        playerConfiguration.metaData = playerConfiguration.metaData ?? MetaData()
        playerConfiguration.metaData!.extra = playerConfiguration.metaData!.extra ?? [:]
        playerConfiguration.metaData!.extra![PlayerModule.requestIdTag] = id
        
        config = playerConfiguration
        
        try playerExtensions.forEach { playerExtension in
            try playerExtension.onContentWillLoad(
                jsonPlayerConfiguration: jsonPlayerConfiguration
            )
        }
        
        
        if #available(tvOS 15.0, *) {
            //if let autoPlay = playerConfiguration.autoPlay {
            //    playWhenReady = autoPlay
            //}
            
            if playerConfiguration.drmSystem == .widevine {
                eventEmitter?.emitDrmChanged(playerId: id, drm: "widevine")
            }
            if playerConfiguration.drmSystem == .fairplay {
                eventEmitter?.emitDrmChanged(playerId: id, drm: "fairplay")
            }
                                   
            /**
             * Use white space as fallback, otherwise infobar is not displayed
             */
            let metadata = ChannelMetadata(
                mainTitle: playerConfiguration.metaData?.title,
                subTitle: playerConfiguration.metaData?.byline,
                artworkImage: playerConfiguration.metaData?.posterUrl,
                channelDescription: playerConfiguration.metaData?.metadataDescription)

            let channel = PRESTOplay.ChannelItem (
                playerConfig: playerConfiguration, metadata: metadata, nextChannelInfo: nil
            )
            
            DispatchQueue.main.async { [weak self] in
                guard let self else { return }
     
                guard let playerView = PRESTOplaySDK.shared.playerViewController(
                    channels: [channel],
                    analytics: nil,
                    loopChannels: true
                ) else {
                    
                    let prestoError: PRESTOerror = _PRESTOplaySDK_errors(ErrorType.player_avplayer_error)
                        .addSeverity(PRESTOplay.ErrorSeverity.fatal)
                        .addDetailedMessage("Unsupported video format")
                    handleError(prestoError: prestoError)
                    return
                }
                
                self.playerView = playerView
               
                self.registerListeners()
                
                if let avPlayerInstance = playerView.avPlayerInstance {
                    avPlayerInstance.volume = Float(volume)
                    eventEmitter?.emitVolumeChangedEvent(playerId: id, volume: volume)
                }
                
                
                if let appliesMediaSelectionCriteriaAutomatically = BridgeDeserializer.toAppliesMediaSelectionCriteriaAutomatically(
                    jsonPlayerConfiguration)
                {
                    playerView.avPlayerInstance?.appliesMediaSelectionCriteriaAutomatically = appliesMediaSelectionCriteriaAutomatically
                }
                
                playerView.closeWhenPlaybackEnd = false
                playerView.requiresLinearPlayback = false
                                
                RCTPresentedViewController()?.present(playerView, animated: false) {
                    self.playerExtensions.forEach { playerExtension in
                        playerExtension.onContentLoaded()
                    }
                    completion()
                }
            }
        } else {
            fatalError("PlayerView is only available on tvOS 15.0")
        }
    }
    
    public func sessionId() -> String {
        return self.delegate?.sessionId ?? {
            print("ApplePlayerProxy: componentId: delegate is nil")
            return ""
        }()
    }

    public func play() {
        lock.lock()
        defer {
            lock.unlock()
        }
        playerView?.play()
        //playWhenReady = true
    }

    public func replay() {
        lock.lock()
        defer {
            lock.unlock()
        }

        let startTimeMs = config?.startTime ?? 0

        guard !(self.playerView?.isLive ?? false),
              self.playerView?.playbackState == .ended,
              startTimeMs >= 0
        else {
            return
        }
        
        playerView?.avPlayerInstance?.replaceCurrentItem(with: self.avPlayerItem);
        playerView?.avPlayerInstance?.seek(to: .zero)
        playerView?.play()
    }

    public func pause() {
        lock.lock()
        defer {
            lock.unlock()
        }
        playerView?.pause()
        //playWhenReady = false
    }

    public func stop() {
        lock.lock()
        defer {
            lock.unlock()
        }
        playerView?.close()
        playWhenReady = false
    }

    public func setPosition(_ newPositionMs: Int64) {
        
        lock.lock()
        defer {
            lock.unlock()
        }

        var newPositionSec = Double(newPositionMs) / 1000
        
        if let range = lastSeekableTimeRanges.last,
           let liveStartTime = playerView?.getLiveStartTime()
        {
            newPositionSec = range.start.seconds + newPositionSec
        }

        guard newPositionSec >= 0 else {
            return;
        }
        
        let targetTime = CMTime(seconds: newPositionSec, preferredTimescale: 600)
        playerView?.avPlayerInstance?.seek(to: targetTime)
    }
    
    func setPlaybackRate(_ newPlaybackRate: Double) {
        
        lock.lock()
        defer {
            lock.unlock()
        }

        guard newPlaybackRate >= 0  else {
            return;
        }
        
        if let player = playerView?.avPlayerInstance {
            //let minRate = player.currentItem?.canPlaySlowForward ?? false
            //let maxRate = player.currentItem?.canPlayFastForward ?? false
            //print("Can play slow: \(minRate), fast: \(maxRate)")
            player.rate = Float(newPlaybackRate)
        }
    }
    
    public func getPlaybackRate() -> Double {
        if let player = playerView?.avPlayerInstance {
            return Double(player.rate)
        } else {
            return 1;
        }
    }
    
    public func setVolume(_ newVolume: Double) {
        lock.lock()
        defer {
            lock.unlock()
        }

        guard newVolume >= 0 && newVolume <= 1 else {
            return;
        }

        if let player = playerView?.avPlayerInstance {
            volume = newVolume
            player.volume = Float(volume)
            eventEmitter?.emitVolumeChangedEvent(playerId: id, volume: volume)
        }
    }
    
    public func setMuted(_ newMuted: Bool) {
        lock.lock()
        defer {
            lock.unlock()
        }

        if let player = playerView?.avPlayerInstance {
            if (newMuted) {
                player.volume = 0
            } else {
                player.volume = Float(volume)
            }
            eventEmitter?.emitMutedChangedEvent(playerId: id, muted: newMuted);
        }
    }

    public func setAudioTrack(_ trackId: String) {
        lock.lock()
        defer {
            lock.unlock()
        }
        
        guard let currentItem = playerView?.avPlayerInstance?.currentItem else { return }
        if let audioGroup = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .audible) {
            
            for option in audioGroup.options {
                let optionTrackId = BridgeSerializer.mediaOptionIdentifier(option)
                if (trackId == optionTrackId) {
                    currentItem.select(option, in: audioGroup)
                    Task {
                        await onTrackModelChanged()
                    }
                    break;
                }
            }
        }
    }

    public func setVideoTrack(_: String) {
        print("setVideoTrack: Not implemented")
    }

    public func setVideoRendition(_: String) {
        print("setVideoRendition: Not implemented")
    }
    
    public func enableAdaptiveVideo() {
        print("enableAdaptiveVideo: Not implemented")
    }

    public func setTextTrack(_ trackId: String?) {
        lock.lock()
        defer {
            lock.unlock()
        }
        
        guard let currentItem = playerView?.avPlayerInstance?.currentItem else { return }
        if let subtitleGroup = currentItem.asset.mediaSelectionGroup(forMediaCharacteristic: .legible) {
            
            if (trackId == nil) {
                currentItem.select(nil, in: subtitleGroup)
            } else {
                
                for option in subtitleGroup.options {
                    let optionTrackId = BridgeSerializer.mediaOptionIdentifier(option)
                    if (trackId == optionTrackId) {
                        currentItem.select(option, in: subtitleGroup)
                        Task {
                            await onTrackModelChanged()
                        }
                        break;
                    }
                }
            }
        }
    }

    public func supportsPictureInPicture() -> Bool {
        lock.lock()
        defer {
            lock.unlock()
        }
        if #available(tvOS 15.0, *) {
            guard let playerView else { return false }
            return playerView.allowsPictureInPicturePlayback
        } else {
            return false
        }
    }

    public func isInPictureInPictureMode() -> Bool {
        print("isInPictureInPictureMode: Not implemented")
        return false
    }

    public func enterPictureInPictureMode() {
        print("enterPictureInPictureMode: Not implemented")
    }

    public func exitPictureInPictureMode() {
        print("exitPictureInPictureMode: Not implemented")
    }

    public func skip(intervalMs: Int64) {
        setPosition(positionMs + intervalMs)
    }

    public func attachToView(_ superView: UIView) {
        attachToView(superView, lock: lock)
    }

    public func updateSize(_ rect: CGRect) {
        updateSize(rect, lock: lock)
    }

    public func detachFromView() {
        lock.lock()
        defer {
            lock.unlock()
        }
        playerView?.removeFromParent()
        playerView?.view.removeFromSuperview()

        pendingSuperView = nil
        pendingSize = nil
    }
    
    func addExtension(playerExtension: PlayerExtension) {
        lock.lock()
        defer {
            lock.unlock()
        }
        self.playerExtensions.append(playerExtension)
    }

    func getExtension<T: PlayerExtension>(of type: T.Type) -> T? {
        lock.lock()
        defer {
            lock.unlock()
        }
        for playerExtension in playerExtensions {
            if let instance = playerExtension as? T {
                return instance
            }
        }
        return nil
    }
    
    func getViewController() -> PRESTOplay.PlayerViewControllerAPI? {
        lock.lock()
        defer {
            lock.unlock()
        }
        return self.playerView
    }


    // MARK: Internal
    
    private func handleError (prestoError: PRESTOerror) {
        self.avPlayerItem = nil;
        self.playerView?.close();
        self.eventEmitter?.emitError(playerId: self.id, error: prestoError)
    }

    func registerListeners() {
        
        if let player = playerView?.avPlayerInstance {
            
            player.publisher(for: \.rate)
                .sink { [weak self] rate in
                    guard let self = self else { return }
                    eventEmitter?.emitPlaybackRateChanged(playerId: id, playbackRate: Double(rate))
                }
                .store(in: &avPlayerCancellables)
            
            // Observe when the currentItem changes
            player.publisher(for: \.currentItem)
            .sink { [weak self] currentItem in
                guard let self = self else { return }

                // Cancel all old item-specific subscriptions
                self.itemCancellables.removeAll()
                //print("Current item changed: ", currentItem)
                guard let item = currentItem else {
                    // Reset item as it is deleted on .ended
                    if self.avPlayerItem != nil && player.status != .failed {
                        player.replaceCurrentItem(with: self.avPlayerItem);
                        player.seek(to: .zero)
                        player.pause()
                    }
                    return
                }
                
                self.avPlayerItem = item

                // Observe item duration
                item.publisher(for: \.duration)
                    .sink { [weak self] duration in
                        guard let self = self else { return }
                        if duration.isNumeric {
                            let durationMs =  BridgeSerializer.toMilliseconds(seconds: duration.seconds)
                            self.eventEmitter?.emitDurationChanged(
                                playerId: self.id,
                                durationMs: durationMs
                            )
                        }
                    }
                    .store(in: &self.itemCancellables)
                
                item.publisher(for: \.currentMediaSelection)
                    .sink { [weak self] currentMediaSelection in
                        guard let self = self else { return }
                        // print ("--- currentMediaSelection: ", currentMediaSelection)
                        Task {
                            await self.onTrackModelChanged()
                        }
                    }
                    .store(in: &self.itemCancellables)

                item.publisher(for: \.status)
                    .sink { [self] status in
                        switch status {
                        case .readyToPlay:
                            self.eventEmitter?.emitLiveChanged(
                                playerId: self.id,
                                isLive: self.playerView?.isLive ?? false
                            )
                            break;
                        case .unknown: break
                        case .failed: break;
                        @unknown default: break;
                        }
                    }
                    .store(in: &self.itemCancellables)
/*
                item.publisher(for: \.tracks)
                    .sink { tracks in
                        print ("--- TRACKS: ", tracks)
                    }
                    .store(in: &self.itemCancellables)
                
                item.publisher(for: \.timedMetadata)
                    .sink { timedMetadata in
                        print ("--- timedMetadata: ", timedMetadata)
                    }
                    .store(in: &self.itemCancellables)
*/
                item.publisher(for: \.error)
                    .sink { [weak self] value in
                        
                        guard let self,
                              let nsError = value as NSError? else { return }
                        
                        var prestoError: PRESTOerror;
                        
                        if nsError.domain == NSURLErrorDomain {
                            prestoError = _PRESTOplaySDK_errors(ErrorType.network_http_bad_status_code)
                                .addParentError(nsError)
                                .addSeverity(PRESTOplay.ErrorSeverity.fatal)
                                .addDetailedMessage(nsError.localizedDescription)
                        } else {
                            prestoError = _PRESTOplaySDK_errors(ErrorType.player_avplayer_error)
                                .addParentError(nsError)
                                .addSeverity(PRESTOplay.ErrorSeverity.fatal)
                                .addDetailedMessage(nsError.localizedDescription)
                        }
                        
                        handleError(prestoError: prestoError)
                    }
                    .store(in: &self.itemCancellables)
            }
            .store(in: &avPlayerCancellables)
        }
        
        
        playerView?.onState = { [weak self] prev, current in
            guard let self = self else { return }
            guard let eventEmitter = self.eventEmitter else { return }
            
            if prev != current {
                eventEmitter.emitStateChanged(
                    playerId: self.id,
                    state: current
                )
            }

            switch current {
            case .error:
                if let error = playerView?.avPlayerInstance?.error {
                    var prestoError: PRESTOerror = _PRESTOplaySDK_errors(ErrorType.player_avplayer_error).addParentError(error)
                    prestoError = prestoError.addSeverity(PRESTOplay.ErrorSeverity.fatal)
                    prestoError = prestoError.addDetailedMessage(error.localizedDescription)
                    handleError(prestoError: prestoError)
                }
            case .ready:
                
                break
            case .play:
                if false == self.playWhenReady {
                    eventEmitter.emitPlayWhenReadyChanged(
                        playerId: self.id,
                        playWhenReady: true
                    )
                    self.playWhenReady = true
                }
                break

            case .paused://, .ended:
                if true == self.playWhenReady {
                    eventEmitter.emitPlayWhenReadyChanged(
                        playerId: self.id,
                        playWhenReady: false
                    )
                    self.playWhenReady = false
                }
                break
            default:
                //print("UNHANDLED STATE: ", current)
                break
            }
        }

        playerView?.onStats = { [weak self] stats in
            guard let self = self else { return }
            guard let eventEmitter = self.eventEmitter else { return }
            eventEmitter.emitPlaybackStats(playerId: self.id, stats: stats, bufferFullnessSec: self.bufferFullness);
        }
        
        var lastPositionMs: Int64? = nil
        var lastLiveStartTimeMs: Int64? = nil
        
        timeObserverToken = playerView?.avPlayerInstance?.addPeriodicTimeObserver(
            forInterval: CMTime(seconds: 1, preferredTimescale: 1),
            queue: .main
        ) { [self] time in
            
            if let eventEmitter = self.eventEmitter,
               let playerView = self.playerView
            {
                let liveStartTimeMs = BridgeSerializer.toMillisecondsOrNil(seconds: playerView.getLiveStartTime())
                
                playerExtensions.forEach { playerExtension in
                    playerExtension.onPositionChanged(
                        positionSec: CMTimeGetSeconds(time),
                        durationSec: self.durationSec,
                        isLive: self.playerView?.isLive ?? false,
                        liveStartTime: playerView.getLiveStartTime(),
                        seekRangeStart: self.playerView?.seekableTimeRanges.last?.start.seconds,
                        seekRangeEnd: self.playerView?.seekableTimeRanges.last?.end.seconds
                    )
                }
                
                if let positionMs = getPositionMs() {
                    if (positionMs != lastPositionMs || liveStartTimeMs != lastLiveStartTimeMs) {
                        eventEmitter.emitPositionChanged(playerId: self.id, positionMs: positionMs, liveStartTimeMs: liveStartTimeMs)
                    }
                    lastPositionMs = positionMs
                    lastLiveStartTimeMs = liveStartTimeMs
                }
                
                let seekableTimeRanges = playerView.seekableTimeRanges
                if (seekableTimeRanges != lastSeekableTimeRanges || liveStartTimeMs != lastLiveStartTimeMs) {
                    lastSeekableTimeRanges = seekableTimeRanges
                    lastLiveStartTimeMs = liveStartTimeMs
                    if let range = seekableTimeRanges.last {
                        var startTimeMs: Int64 = BridgeSerializer.toMilliseconds(seconds: range.start.seconds)
                        var endTimeMs: Int64 = BridgeSerializer.toMilliseconds(seconds: range.end.seconds)
                        
                        if let liveStartTimeMs = liveStartTimeMs {
                            startTimeMs = liveStartTimeMs
                            endTimeMs = (liveStartTimeMs + endTimeMs - startTimeMs)
                        }
                        eventEmitter.emitSeekableRangeChanged(playerId: self.id, startTimeMs: startTimeMs, endTimeMs: endTimeMs)
                    }
                }
            }
        }
    }
    
    /*
     * https://castlabs.atlassian.net/browse/RN-665
     */
    private func getPositionMs() -> Int64? {
        guard let positionSec = playerView?.position else {
            return nil
        }
        let positionMs = BridgeSerializer.toMilliseconds(seconds: positionSec)
        if
            playerView?.getLiveStartTime() != nil,
            let seekStart = playerView?.seekableTimeRanges.last?.start.seconds
        {
            let seekStartMs = BridgeSerializer.toMilliseconds(seconds:seekStart)
            return positionMs - seekStartMs
        }

        return positionMs
    }

    private func onTrackModelChanged() async {
        guard let currentItem = playerView?.avPlayerInstance?.currentItem else { return }
        let asset = await currentItem.asset
        
        let currentSelection = await currentItem.currentMediaSelection
        
        var selectedAudio: AVMediaSelectionOption? = nil
        var audioGroup: AVMediaSelectionGroup? = nil
        if let group = asset.mediaSelectionGroup(forMediaCharacteristic: .audible) {
            audioGroup = group
            selectedAudio = currentSelection.selectedMediaOption(in: group)
        }
        
        var selectedSubtitle: AVMediaSelectionOption? = nil
        var subtitleGroup: AVMediaSelectionGroup? = nil
        if let group = asset.mediaSelectionGroup(forMediaCharacteristic: .legible) {
            subtitleGroup = group
            selectedSubtitle = currentSelection.selectedMediaOption(in: group)
        }
        
        try? await eventEmitter?.emitTrackModelChanged(
            playerId: id,
            videoTracks: [],
            audioTracks: audioGroup,
            textTracks: subtitleGroup,
            currentAudioTrack: selectedAudio,
            currentTextTrack: selectedSubtitle,
            isAdaptationModeManual: false
        )
    }

    func unregisterListeners() {
        playerView?.onState = nil
        playerView?.onStats = nil
        if let token = timeObserverToken {
            playerView?.avPlayerInstance?.removeTimeObserver(token)
            timeObserverToken = nil
        }
        avPlayerCancellables.removeAll()
        itemCancellables.removeAll()
    }


    // MARK: Private

    private let lock = NSLock()

    private var playerView: PlayerViewControllerAPI?
    private var config: PlayerConfiguration?
    private var eventEmitter: PlayerEventEmitter?
    private var playWhenReady = false
    
    private var playerExtensions: [PlayerExtension] = []
    
    private var timeObserverToken: Any?
    private var itemCancellables = Set<AnyCancellable>()
    private var avPlayerCancellables = Set<AnyCancellable>()
    
    private var lastSeekableTimeRanges: [CMTimeRange] = [];

    public let delegate: (any PlayerAPI)? = nil
    
    // Needed to replay
    private var avPlayerItem: AVPlayerItem? = nil

    private var volume: Double = 1
    private var durationSec: Double = 0
    
    private var pendingSuperView: UIView?
    private var pendingSize: CGRect?

    private var initialized: Bool { playerView != nil }

    private func sideloadedTracks(type: TrackType) -> [SideloadedTrack]? {
        guard
            let sideloadedTrack = config?.sideloadedTracks?
                .filter({ $0.trackType == type })
        else {
            return nil
        }
        return sideloadedTrack
    }

    private func attachToViewIfPending() {
        if let superView = pendingSuperView {
            attachToView(superView, lock: nil)
            pendingSuperView = nil
        }
    }

    private func updateSizeIfPending() {
        if let size = pendingSize {
            updateSize(size, lock: nil)
            pendingSize = nil
        }
    }

    private func attachToView(_ superView: UIView, lock: NSLock?) {
        lock?.lock()
        defer {
            lock?.unlock()
        }

        if false == initialized {
            pendingSuperView = superView
            return
        }
        let superViewController = superView.reactViewController()

        guard let playerView else { return }
        guard let superViewController else { return }

        superViewController.addChild(playerView)
        superView.addSubview(playerView.view)

        patchPlayerNotReady()
    }

    private func patchPlayerNotReady() {
        guard let playerView else { return }

        // The AVPlayerViewController isn't fully functional after attaching
        // to the view. Force initialisation.
        if playerView.playbackState == .idle {
            if playWhenReady {
                playerView.play()
            } else {
                playerView.seek(playerView.getCurrentTime())
            }
        }
    }

    private func updateSize(_ rect: CGRect, lock: NSLock?) {
        lock?.lock()
        defer {
            lock?.unlock()
        }

        if false == initialized {
            pendingSize = rect
            return
        }
        playerView?.view.frame = rect
    }
    
    private var positionMs: Int64 {
        let time = playerView?.avPlayerInstance?.currentTime() ?? .zero
        let seconds = CMTimeGetSeconds(time)
        guard seconds.isFinite else { return 0 }
        return Int64((seconds * 1000).rounded())
    }
    
    private func everyPlugin(callback: (PluginProtocol) -> Void) {
        var index = 0
        while(index < Repository.shared.plugins.count) {
            if let plugin = Repository.shared.plugins.get(at: index) {
                callback(plugin)
            }

            index += 1
        }
    }
    
    private var bufferFullness: Float {
        get {
            if  let player = playerView?.avPlayerInstance,
                let timeRange = player.currentItem?.loadedTimeRanges.first?.timeRangeValue {
                    let bufferedStart = CMTimeGetSeconds(timeRange.start)
                    let bufferedDuration = CMTimeGetSeconds(timeRange.duration)
                    let bufferedEnd = bufferedStart + bufferedDuration
                
                    let currentTime = CMTimeGetSeconds(player.currentTime())
                    let bufferAhead = bufferedEnd - currentTime
                
                return Float(bufferAhead)
            } else {
                return 0
            }
        }
    }
}
