import AVKit
import Foundation

import PRESTOplay

// MARK: - PlayerProxy

@available(tvOS 14.0, *)
class PlayerProxy: NSObject, PlayerProtocol {
    public let id: String

    // MARK: Lifecycle

    init(playerId: String, playerEventEmitter: PlayerEventEmitter) {
        id = playerId

        eventEmitter = playerEventEmitter
    
        super.init()
        
        volume = player.playbackVolume

        registerListeners()

        // The flag enables background audio playback for VTPlayer, but has no effect on AVPlayer.
        // AVPlayer supports picture-in-picture mode, while VTPlayer does not.
        // This helps compensate for AVPlayer's lack of background audio playback.
        player.continueAudioPlaybackInBackground = true

        everyPlugin { plugin in
            plugin.onPlayerCreated(player: self, playerEventEmitter: playerEventEmitter)
        }
    }

    // FIXME: This crashes the app because destroy() is called multiple times
    deinit {
    if destroyed == false {
        destroy()
      }
    }

    // MARK: Public

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

        playerExtensions.forEach { playerExtension in
            playerExtension.onPlayerWillDestroy()
        }

        everyPlugin { plugin in
            plugin.onPlayerWillDestroy(player: self)
        }

        if #available(tvOS 14.0, *) {
            if let pipController = player.getPictureInPictureController() {
                pipController.stopPictureInPicture()
                pipController.delegate = nil
            }
        }

        playerExtensions = []


        player.stop()
        player.detachFromView()

        unregisterListeners()

        eventEmitter = nil
        destroyed = true
    }
    
    public func sessionId() -> String {
        return player.sessionId
    }

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

        guard player.playbackState != .ended else { return }

        player.play()
    }

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

        let startTimeSec = config?.startTime ?? 0

        guard !player.isLive,
              player.playbackState == .ended,
              startTimeSec >= 0
        else { return }

        player.seek(startTimeSec)

        player.play()
    }

    public func pause() {
        lock.lock()
        defer {
            lock.unlock()
        }
        player.pause()
    }

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

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

        try playerExtensions.forEach { playerExtension in
            try playerExtension.onContentWillLoad(
                jsonPlayerConfiguration: jsonPlayerConfiguration
            )
        }

        let playerConfiguration = try BridgeDeserializer.toPlayerConfiguration(
            jsonPlayerConfiguration
        )
        
        playerConfiguration.metaData = playerConfiguration.metaData ?? MetaData()
        playerConfiguration.metaData!.extra = playerConfiguration.metaData!.extra ?? [:]
        playerConfiguration.metaData!.extra![PlayerModule.requestIdTag] = id
        
        // Check if it is a downloaded content and set originUrl
        if playerConfiguration.contentUrl.scheme == "file" {
            for download in PRESTOplaySDK.shared.downloader().getDownloads() {
                if download.localUrl == playerConfiguration.contentUrl {
                    playerConfiguration.originUrl = download.playerConfiguration.contentUrl
                    playerConfiguration.drmType = download.playerConfiguration.drmType
                    playerConfiguration.drmSystem = download.playerConfiguration.drmSystem
                    playerConfiguration.drmConfiguration = download.playerConfiguration.drmConfiguration
                    break;
                }
            }
        }

        let silentSwitchBehavior = try BridgeDeserializer.toSilentSwitchBehaviour(
            jsonPlayerConfiguration
        )
        
        // Reset the anti-duplication event mechanism to ensure all events are delivered.
        // TODO: Instead of resetting the entire cache, remove entries related to this instance player
        eventEmitter?.resetEventDetailsCache()

        if playerConfiguration.drmSystem == .widevine {
            eventEmitter?.emitDrmChanged(playerId: id, drm: "widevine")
        }
        if playerConfiguration.drmSystem == .fairplay {
            eventEmitter?.emitDrmChanged(playerId: id, drm: "fairplay")
        }

        eventEmitter?.emitPlaybackRateChanged(playerId: id, playbackRate: player.playbackRate)

        config = playerConfiguration

        DispatchQueue.main.async { [weak self] in
            self?.player.load(config: playerConfiguration)
            
            /**
             * https://castlabs.atlassian.net/browse/IOSP-4566
             * Workaround for offline DASH content with startTime, which does not work.
             * The behaviour of PlayerConfiguration.startTime when playing offline DASH content is strange:
             *   It sets the player position right, player status is Playing, but then actual playback does not start,
             *   until the defined amount of seconds passes.
             */
            if let startTime = playerConfiguration.startTime,
                playerConfiguration.contentUrl.scheme == "file",
                startTime > 0,
                self?.player.engine == PlayerEngine.castlabs
            {
                self?.player.minPrebufferTime = 60.0
                self?.player.maxPrebufferTime = 120.0
                self?.player.minRebufferTime = 60.0
            }
            
            if let appliesMediaSelectionCriteriaAutomatically = BridgeDeserializer.toAppliesMediaSelectionCriteriaAutomatically(
                jsonPlayerConfiguration)
            {
                self?.player.avPlayerInstance?.appliesMediaSelectionCriteriaAutomatically = appliesMediaSelectionCriteriaAutomatically
            }
            
            // IOSP-4506: Add audiovisualBackgroundPlaybackPolicy to HLSPlayer
            if #available(iOS 15.0, tvOS 15.0, *) {
                if (self?.player.engine == PlayerEngine.apple) {
                    self?.player.avPlayerInstance?.audiovisualBackgroundPlaybackPolicy = .continuesIfPossible
                }
            }
            
            self?.playerExtensions.forEach { playerExtension in
                playerExtension.onContentLoaded()
            }
            self?.setupSilentSwitch(silentSwitchBehaviour: silentSwitchBehavior)

            self?.player.open(autoplay: playerConfiguration.autoPlay ?? false)
            
            completion()
        }
        eventEmitter?.emitVolumeChangedEvent(playerId: id, volume: volume)
    }

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

        // HDPRN-89, RN-506:
        //
        // When a fatal error is thrown the player is in the Error state.
        // The JavaScript player clears any fatal errors when it enters the Stopping state.
        //
        // The HlsPlayer doesn't trigger the Stopping or Idle states when Player.stop() is called.
        // In contrast, the VtPlayer correctly transitions to these states.
        // To ensure consistent behavior across implementations,
        // we manually trigger the Stopping and Idle states.
        //
        // TODO create a bug report for HlsPlayer
        eventEmitter?.emitStateChanged(playerId: id, state: .stopping)
        eventEmitter?.emitStateChanged(playerId: id, state: .idle)

        guard player.playbackState != .idle else { return }

        playerExtensions.forEach { playerExtension in
            playerExtension.onPlayerWillStop()
        }
        player.stop()
    }

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

        var newPositionSec = Double(newPositionMs) / 1000
        
        if (isAppleEnginePlayingLive()) {
            if let range = lastSeekableTimeRanges.last
            {
                newPositionSec = range.start.seconds + newPositionSec
            }
        }
        guard newPositionSec >= 0 else {
            return;
        }
        player.seek(newPositionSec)
    }

    public func setPlaybackRate(_ newPlaybackRate: Double) {
        lock.lock()
        defer {
            lock.unlock()
        }

        guard newPlaybackRate >= 0  else {
            return;
        }

        player.playbackRate = newPlaybackRate
        eventEmitter?.emitPlaybackRateChanged(playerId: id, playbackRate: newPlaybackRate)
    }
    
    public func getPlaybackRate() -> Double {
        return player.playbackRate
    }

    public func setVolume(_ newVolume: Double) {
        lock.lock()
        defer {
            lock.unlock()
        }

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

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

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

    public func setAudioTrack(_ trackId: String) {
        lock.lock()
        defer {
            lock.unlock()
        }

        if let track = player.tracks.audio.first(where: { $0.id == trackId }) {
            player.setAudioTrack(track)
            onTrackModelChanged()
        }
    }

    public func setVideoTrack(_ trackId: String) {
        lock.lock()
        defer {
            lock.unlock()
        }

        if let track = player.tracks.video.first(where: { $0.id == trackId }) {
            player.setVideoTrack(track)
            onTrackModelChanged()
        }
    }

    public func setVideoRendition(_ trackIdAndRenditionId: String) {
        lock.lock()
        defer {
            lock.unlock()
        }

        let components = trackIdAndRenditionId.components(separatedBy: "-")
        guard components.count == 2 else {
            return
        }

        let trackId = components[0]
        let renditionId = components[1]

        if
            let track = player.tracks.video.first(where: { $0.id == trackId }),
            let rendition = track.renditions.first(where: { $0.id == renditionId })
        {
            player.setVideoRendition(rendition)
            onTrackModelChanged()
        }
    }

    public func setTextTrack(_ trackId: String?) {
        lock.lock()
        defer {
            lock.unlock()
        }

        if trackId == nil {

            // disable text overlay
            player.setTextTrack(nil)
            onTrackModelChanged()
            return;
        }

        if let track = player.tracks.text.first(where: { $0.id == trackId }) {
            player.setTextTrack(track)
            onTrackModelChanged()
        }

    }

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

        player.setVideoRendition(nil)
        onTrackModelChanged()
    }

    public func supportsPictureInPicture() -> Bool {
        if #available(tvOS 14.0, *) {
            lock.lock()
            defer {
                lock.unlock()
            }

            return self.pipController != nil
        } else {
            return false
        }
    }

    public func isInPictureInPictureMode() -> Bool {
        if #available(tvOS 14.0, *) {
            lock.lock()
            defer {
                lock.unlock()
            }

            guard let pipController else {
                return false
            }

            return pipController.isPictureInPictureActive
        } else {
            return false
        }
    }

    public func enterPictureInPictureMode() {
        if #available(tvOS 14.0, *) {
            lock.lock()
            defer {
                lock.unlock()
            }

            pipController?.startPictureInPicture()
        }
    }

    public func exitPictureInPictureMode() {
        if #available(tvOS 14.0, *) {
            lock.lock()
            defer {
                lock.unlock()
            }

            pipController?.stopPictureInPicture()
        }
    }

    public func attachToView(_ to: UIView) {
        lock.lock()
        defer {
            lock.unlock()
        }

        DispatchQueue.main.async { [weak self] in
            self?.player.attach(to: to.layer)
        }
    }

    public func updateSize(_ rect: CGRect) {
        lock.lock()
        defer {
            lock.unlock()
        }
        player.update(size: rect.size)
    }

    public func detachFromView() {
        lock.lock()
        defer {
            lock.unlock()
        }
        player.detachFromView()
    }

    // MARK: Internal

    func registerListeners() {
        player.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:
                /**
                 * Probably errors should not be emitted here, as errors are also emmited
                 * in PlayerModule in the global sdk error handler.
                 * Not all errors are emitted here ie: drm expired.
                 * TODO: TO BE TESTED.
                 */
                if let error = player.error {
                    eventEmitter.emitError(playerId: self.id, error: error)
                }
            case .ready:
                self.pipController = player.getPictureInPictureController()
                self.pipController?.delegate = self

                eventEmitter.emitLiveChanged(
                    playerId: self.id,
                    isLive: player.isLive
                )

                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:
                break
            }
        }

        player.onTrackModel = { [weak self] in
            guard let self = self else { return }
            self.onTrackModelChanged()
        }

        player.onDuration = { [weak self] duration in
            guard let self = self else { return }
            guard let eventEmitter = self.eventEmitter else { return }

            if !player.isLive && duration > 0 {
                eventEmitter.emitDurationChanged(
                    playerId: self.id,
                    durationMs: BridgeSerializer.toMilliseconds(seconds: duration)
                )
            }
        }
        
        // Updates position every 1s
        player.onCurrentTime(rate: 1.0) { [weak self] positionSec in
            guard let self = self else { return }
            guard let eventEmitter = self.eventEmitter else { return }
            
            let liveStartTimeMs = BridgeSerializer.toMillisecondsOrNil(seconds: player.getLiveStartTime())
            let seekableTimeRanges = player.seekableTimeRanges

            playerExtensions.forEach { playerExtension in
                playerExtension.onPositionChanged(
                    positionSec: positionSec,
                    durationSec: self.player.duration,
                    isLive: self.player.isLive,
                    liveStartTime: self.player.getLiveStartTime(),
                    seekRangeStart: seekableTimeRanges.last?.start.seconds,
                    seekRangeEnd: seekableTimeRanges.last?.end.seconds
                )
            }
            
            if let positionMs = getPositionMs() {
                eventEmitter.emitPositionChanged(playerId: self.id, positionMs: positionMs, liveStartTimeMs: liveStartTimeMs)
            }
            
            if (seekableTimeRanges != lastSeekableTimeRanges || liveStartTimeMs != lastLiveStartTimeMs) {
                lastSeekableTimeRanges = seekableTimeRanges;
                if let range = seekableTimeRanges.last {
                    var startTimeMs: Int64 = BridgeSerializer.toMilliseconds(seconds: range.start.seconds)
                    var endTimeMs: Int64 = BridgeSerializer.toMilliseconds(seconds: range.end.seconds)

                    if (isAppleEnginePlayingLive()) {
                        if let liveStartTimeMs = liveStartTimeMs {
                            let durationMs = endTimeMs - startTimeMs
                            startTimeMs = liveStartTimeMs
                            endTimeMs = liveStartTimeMs + durationMs
                        }
                    }
                    eventEmitter.emitSeekableRangeChanged(playerId: self.id, startTimeMs: startTimeMs, endTimeMs: endTimeMs)
                }
            }
            if let stats = player.getStats() {
                if bufferFullness != player.bufferFullness {
                    bufferFullness = player.bufferFullness
                    eventEmitter.emitPlaybackStats(playerId: self.id, stats: stats, bufferFullnessSec: bufferFullness);
                }
            }
            lastSeekableTimeRanges = seekableTimeRanges
            lastLiveStartTimeMs = liveStartTimeMs
        }

        player.onStats = { [weak self] stats in
            guard let self = self else { return }
            guard let eventEmitter = self.eventEmitter else { return }
            bufferFullness = player.bufferFullness
            eventEmitter.emitPlaybackStats(playerId: self.id, stats: stats, bufferFullnessSec: bufferFullness);
        }

        player.onLiveStartTime = { [weak self] liveStartTime in
            guard let self = self else { return }
            guard let eventEmitter = self.eventEmitter else { return }
            if self.player.playbackState != .playing {
                if let positionMs = getPositionMs() {
                    eventEmitter.emitPositionChanged(
                        playerId: self.id,
                        positionMs: positionMs,
                        liveStartTimeMs: BridgeSerializer.toMilliseconds(seconds: liveStartTime)
                    )
                }
            }
        }
    }
    
    /*
     * https://castlabs.atlassian.net/browse/RN-665
     */
    private func isAppleEnginePlayingLive() -> Bool {
        return player.engine == PlayerEngine.apple && player.getLiveStartTime() != nil
    }
    
    private func getPositionMs() -> Int64? {
        guard let positionSec = player.position else {
            return nil
        }
        
        let positionMs = BridgeSerializer.toMilliseconds(seconds: positionSec)
        
        if (isAppleEnginePlayingLive()) {
            if let seekStart = player.seekableTimeRanges.last?.start.seconds {
                return positionMs - BridgeSerializer.toMilliseconds(seconds: seekStart)
            }
        }
        
        return positionMs
    }

    func unregisterListeners() {
        player.onState = nil
        player.onTrackModel = nil
        player.onDuration = nil
        player.onStats = nil
        player.onLiveStartTime = nil

        // TODO Create a ticket for iOS SDK improvment
        //
        // iOS SDK doesn't allow to nullify `player.onCurrentTime`.
        // When setting an empty callback with the weak reference to the self,
        // it fails because the instance is already in the deinit phase:
        //
        // objc[1454]: Cannot form weak reference to instance (0x1353946c0) of class react_native_prestoplay.PlayerProxy.
        // It is possible that this object was over-released, or is in the process of deallocation.
    }

    func setupSilentSwitch(
        silentSwitchBehaviour: SilentSwitchBehaviour
    ) {
        /// ignore silent switch is intentionally implemented in open source part of the sdk
        /// as there are many use cases like mixing audio, which should not be forced
        /// by PRESTOplay SDK v4 and should be considered case by case by the SDK user
        do {
            switch silentSwitchBehaviour {
            case .ignore:
                try AVAudioSession.sharedInstance().setCategory(.playback)
            case .obey:
                try AVAudioSession.sharedInstance().setCategory(.ambient)
            default:
                print("AVAudioSession will use default silent switch behaviour")
            }
        } catch { }
    }

    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
    }

    public var delegate: (any PlayerAPI)? {
        get {
            return player
        }
    }
    
    func getViewController() -> PRESTOplay.PlayerViewControllerAPI? {
        return nil
    }

    // MARK: Private

    private var destroyed: Bool = false

    private var player = PRESTOplaySDK.shared.player()

    private let lock = NSRecursiveLock()
    private var playerExtensions: [PlayerExtension] = []

    private var config: PlayerConfiguration?
    private var eventEmitter: PlayerEventEmitter?
    private var pipController: AVPictureInPictureController?

    private var playWhenReady = false

    private var networkModifiersAdded = false
    private var volume: Double = 1;
    private var bufferFullness:Float? = nil;

    private var positionMs: Int64 {
        get {
            let positionSec = player.position ?? 0
            return Int64(positionSec * 1000)
        }
    }
    
    private var lastSeekableTimeRanges: [CMTimeRange] = [];
    private var lastLiveStartTimeMs: Int64? = nil;

    private func onTrackModelChanged() {
        eventEmitter?.emitTrackModelChanged(
            playerId: id,
            videoTracks: player.tracks.video,
            audioTracks: player.tracks.audio,
            textTracks: player.tracks.text,
            currentVideoTrack: player.getVideoTrack(),
            currentAudioTrack: player.getAudioTrack(),
            currentTextTrack: player.getTextTrack(),
            currentVideoRendition: player.getSelectedVideoRendition(),
            isAdaptationModeManual: player.isAdaptationModeManual
        )
    }

    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
        }
    }
}

// MARK: AVPictureInPictureControllerDelegate

@available(tvOS 14.0, *)
extension PlayerProxy: AVPictureInPictureControllerDelegate {

    public func pictureInPictureControllerWillStartPictureInPicture(
        _: AVPictureInPictureController
    ) {
        eventEmitter?.emitPictureInPictureModeChanged(
            playerId: id,
            pictureInPictureMode: true
        )
    }

    // swiftlint:disable line_length
    public func pictureInPictureController(
        _: AVPictureInPictureController,
        restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (
            Bool
        ) -> Void
    ) {
        completionHandler(true)
    }

    // swiftlint:enable

    public func pictureInPictureControllerDidStopPictureInPicture(
        _: AVPictureInPictureController
    ) {
        eventEmitter?.emitPictureInPictureModeChanged(
            playerId: id,
            pictureInPictureMode: false
        )
    }
}
