import Foundation
import SwiftSignalKit
import AVFoundation
import MobileCoreServices
import Display
import Postbox
import TelegramCore
import MediaPlayer
import TelegramAudio
import UniversalMediaPlayer
import TelegramPresentationData
import TelegramUIPreferences
import AccountContext
import TelegramUniversalVideoContent
import DeviceProximity
import MediaResources
import PhotoResources

enum SharedMediaPlayerGroup: Int {
    case music = 0
    case voiceAndInstantVideo = 1
}

private let sharedAudioSession: ManagedAudioSession = {
    let audioSession = ManagedAudioSessionImpl()
    let _ = (audioSession.headsetConnected() |> deliverOnMainQueue).startStandalone(next: { value in
        DeviceProximityManager.shared().setGloballyEnabled(!value)
    })
    return audioSession
}()

private struct GlobalControlOptions: OptionSet {
    var rawValue: Int32
    
    init(rawValue: Int32 = 0) {
        self.rawValue = rawValue
    }
    
    static let play = GlobalControlOptions(rawValue: 1 << 0)
    static let pause = GlobalControlOptions(rawValue: 1 << 1)
    static let previous = GlobalControlOptions(rawValue: 1 << 2)
    static let next = GlobalControlOptions(rawValue: 1 << 3)
    static let playPause = GlobalControlOptions(rawValue: 1 << 4)
    static let seek = GlobalControlOptions(rawValue: 1 << 5)
}

public final class MediaManagerImpl: NSObject, MediaManager {
    public static var globalAudioSession: ManagedAudioSession {
        return sharedAudioSession
    }
    
    private let isCurrentPromise = ValuePromise<Bool>(false)
    var isCurrent: Bool = false {
        didSet {
            if self.isCurrent != oldValue {
                self.isCurrentPromise.set(self.isCurrent)
            }
        }
    }
    
    private let queue = Queue.mainQueue()
    
    private let accountManager: AccountManager<TelegramAccountManagerTypes>
    private let inForeground: Signal<Bool, NoError>
    private let presentationData: Signal<PresentationData, NoError>
    
    public let audioSession: ManagedAudioSession
    public let overlayMediaManager: OverlayMediaManager = OverlayMediaManager()
    let sharedVideoContextManager = SharedVideoContextManager()
    
    private var nextPlayerIndex: Int32 = 0
    
    private let voiceMediaPlayerStateDisposable = MetaDisposable()
    private var voiceMediaPlayer: SharedMediaPlayer? {
        didSet {
            if self.voiceMediaPlayer !== oldValue {
                if let voiceMediaPlayer = self.voiceMediaPlayer {
                    let account = voiceMediaPlayer.account
                    self.voiceMediaPlayerStateDisposable.set((voiceMediaPlayer.playbackState
                    |> deliverOnMainQueue).startStrict(next: { [weak self, weak voiceMediaPlayer] state in
                        guard let strongSelf = self else {
                            return
                        }
                        guard let state = state, let voiceMediaPlayer = voiceMediaPlayer else {
                            strongSelf.voiceMediaPlayerStateValue.set(.single(nil))
                            return
                        }
                        if case let .item(item) = state {
                            strongSelf.voiceMediaPlayerStateValue.set(.single((account, .state(item))))
                            let audioLevelValue: (AccountRecordId, SharedMediaPlaylistId, SharedMediaPlaylistItemId, Signal<Float, NoError>)? = (account.id, item.playlistId, item.item.id, voiceMediaPlayer.audioLevel)
                            strongSelf.voiceMediaPlayerAudioLevelEvents.set(.single(audioLevelValue))
                        } else {
                            strongSelf.voiceMediaPlayerStateValue.set(.single((account, .loading)))
                            strongSelf.voiceMediaPlayerAudioLevelEvents.set(.single(nil))
                        }
                    }))
                } else {
                    self.voiceMediaPlayerStateDisposable.set(nil)
                    self.voiceMediaPlayerStateValue.set(.single(nil))
                    self.voiceMediaPlayerAudioLevelEvents.set(.single(nil))
                }
            }
        }
    }
    private let voiceMediaPlayerStateValue = Promise<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?>(nil)
    var voiceMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError> {
        return self.voiceMediaPlayerStateValue.get()
    }
    
    private let voiceMediaPlayerAudioLevelEvents = Promise<(AccountRecordId, SharedMediaPlaylistId, SharedMediaPlaylistItemId, Signal<Float, NoError>)?>(nil)
    
    private var musicMediaPlayer: SharedMediaPlayer? {
        didSet {
            if self.musicMediaPlayer !== oldValue {
                if let musicMediaPlayer = self.musicMediaPlayer {
                    let type = musicMediaPlayer.type
                    let account = musicMediaPlayer.account
                    self.musicMediaPlayerStateValue.set(musicMediaPlayer.playbackState
                    |> map { state -> (Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)? in
                        guard let state = state else {
                            return nil
                        }
                        if case let .item(item) = state {
                            return (account, .state(item), type)
                        } else {
                            return (account, .loading, type)
                        }
                    } |> deliverOnMainQueue)
                } else {
                    self.musicMediaPlayerStateValue.set(.single(nil))
                }
            }
        }
    }
    private let musicMediaPlayerStateValue = Promise<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?>(nil)
    public var musicMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> {
        return self.musicMediaPlayerStateValue.get()
    }
    
    private let globalMediaPlayerStateValue = Promise<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?>()
    public var globalMediaPlayerState: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> {
        return self.globalMediaPlayerStateValue.get()
    }
    public var activeGlobalMediaPlayerAccountId: Signal<(AccountRecordId, Bool)?, NoError> {
        return self.globalMediaPlayerStateValue.get()
        |> map { state -> (AccountRecordId, Bool)? in
            return state.flatMap { state -> (AccountRecordId, Bool) in
                var isPlaying = false
                if case let .state(value) = state.1 {
                    switch value.status.status {
                    case .playing:
                        isPlaying = true
                    case .buffering(_, true, _, _):
                        isPlaying = true
                    default:
                        break
                    }
                }
                return (state.0.id, isPlaying)
            }
        }
        |> distinctUntilChanged(isEqual: { lhs, rhs in
            if lhs?.0 != rhs?.0 {
                return false
            }
            if lhs?.1 != rhs?.1 {
                return false
            }
            return true
        })
    }
    
    private let setPlaylistByTypeDisposables = DisposableDict<MediaManagerPlayerType>()
    private var mediaPlaybackStateDisposable = MetaDisposable()
    
    private let sharedPlayerByGroup: [SharedMediaPlayerGroup: SharedMediaPlayer] = [:]
    private var currentOverlayVideoNode: OverlayMediaItemNode?
    
    private let globalControlsStatus = Promise<MediaPlayerStatus?>(nil)
    
    private let globalControlsDisposable = MetaDisposable()
    private let globalControlsArtworkDisposable = MetaDisposable()
    private let globalControlsArtwork = Promise<(Account, SharedMediaPlaybackAlbumArt)?>(nil)
    private let globalControlsStatusDisposable = MetaDisposable()
    private let globalAudioSessionForegroundDisposable = MetaDisposable()
    
    public let universalVideoManager: UniversalVideoManager = UniversalVideoManagerImpl()
    
    public let galleryHiddenMediaManager: GalleryHiddenMediaManager = GalleryHiddenMediaManagerImpl()
    
    init(accountManager: AccountManager<TelegramAccountManagerTypes>, inForeground: Signal<Bool, NoError>, presentationData: Signal<PresentationData, NoError>) {
        self.accountManager = accountManager
        self.inForeground = inForeground
        self.presentationData = presentationData
        
        self.audioSession = sharedAudioSession
        
        super.init()
       
        let combinedPlayersSignal: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> = combineLatest(queue: Queue.mainQueue(), self.voiceMediaPlayerState, self.musicMediaPlayerState)
        |> map { voice, music -> (Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)? in
            if let voice = voice {
                return (voice.0, voice.1, .voice)
            } else if let music = music {
                return (music.0, music.1, music.2)
            } else {
                return nil
            }
        }
        self.globalMediaPlayerStateValue.set(combinedPlayersSignal
        |> distinctUntilChanged(isEqual: { lhs, rhs in
            return lhs?.0 === rhs?.0 && lhs?.1 == rhs?.1 && lhs?.2 == rhs?.2
        }))
        
        var baseNowPlayingInfo: [String: Any]?
        
        var previousState: SharedMediaPlayerItemPlaybackState?
        var previousDisplayData: SharedMediaPlaybackDisplayData?
        let globalControlsArtwork = self.globalControlsArtwork
        let globalControlsStatus = self.globalControlsStatus
        
        var currentGlobalControlsOptions = GlobalControlOptions()
        
        self.globalControlsDisposable.set((combineLatest(self.globalMediaPlayerState, self.presentationData)
        |> deliverOnMainQueue).startStrict(next: { stateAndType, presentationData in
            var updatedGlobalControlOptions = GlobalControlOptions()
            if let (_, stateOrLoading, type) = stateAndType, case let .state(state) = stateOrLoading {
                if type == .music {
                    updatedGlobalControlOptions.insert(.previous)
                    updatedGlobalControlOptions.insert(.next)
                    updatedGlobalControlOptions.insert(.seek)
                    switch state.status.status {
                        case .playing, .buffering(_, true, _, _):
                            updatedGlobalControlOptions.insert(.pause)
                        default:
                            updatedGlobalControlOptions.insert(.play)
                    }
                }
            }
            
            if let (account, stateOrLoading, type) = stateAndType, type == .music, case let .state(state) = stateOrLoading, let displayData = state.item.displayData {
                if previousDisplayData != displayData {
                    previousDisplayData = displayData
                    
                    var nowPlayingInfo: [String: Any] = [:]
                    
                    var artwork: SharedMediaPlaybackAlbumArt?
                    
                    switch displayData {
                        case let .music(title, performer, artworkValue, _, _):
                            artwork = artworkValue
                            
                            let titleText: String = title ?? presentationData.strings.MediaPlayer_UnknownTrack
                            let subtitleText: String = performer ?? presentationData.strings.MediaPlayer_UnknownArtist
                            
                            nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
                            nowPlayingInfo[MPMediaItemPropertyArtist] = subtitleText
                        case let .voice(author, _):
                            let titleText: String = author?.debugDisplayTitle ?? ""
                            
                            nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
                        case let .instantVideo(author, _, _):
                            let titleText: String = author?.debugDisplayTitle ?? ""
                            
                            nowPlayingInfo[MPMediaItemPropertyTitle] = titleText
                    }
                    
                    globalControlsArtwork.set(.single(artwork.flatMap({ (account, $0) })))
                    
                    baseNowPlayingInfo = nowPlayingInfo
                    
                    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
                }
                
                if previousState != state {
                    previousState = state
                    globalControlsStatus.set(.single(state.status))
                }
            } else {
                previousState = nil
                previousDisplayData = nil
                globalControlsStatus.set(.single(nil))
                globalControlsArtwork.set(.single(nil))
                
                if baseNowPlayingInfo != nil {
                    baseNowPlayingInfo = nil
                    MPNowPlayingInfoCenter.default().nowPlayingInfo = nil
                }
            }
            
            if currentGlobalControlsOptions != updatedGlobalControlOptions {
                let commandCenter = MPRemoteCommandCenter.shared()
                
                var optionsAndCommands: [(GlobalControlOptions, MPRemoteCommand, Selector)] = [
                    (.play, commandCenter.playCommand, #selector(self.playCommandEvent(_:))),
                    (.pause, commandCenter.pauseCommand, #selector(self.pauseCommandEvent(_:))),
                    (.previous, commandCenter.previousTrackCommand, #selector(self.previousTrackCommandEvent(_:))),
                    (.next, commandCenter.nextTrackCommand, #selector(self.nextTrackCommandEvent(_:))),
                    ([.play, .pause], commandCenter.togglePlayPauseCommand, #selector(self.togglePlayPauseCommandEvent(_:)))
                ]
                if #available(iOSApplicationExtension 9.1, iOS 9.1, *) {
                    optionsAndCommands.append((.seek, commandCenter.changePlaybackPositionCommand, #selector(self.changePlaybackPositionCommandEvent(_:))))
                }
                
                for (option, command, selector) in optionsAndCommands {
                    let previousValue = !currentGlobalControlsOptions.intersection(option).isEmpty
                    let updatedValue = !updatedGlobalControlOptions.intersection(option).isEmpty
                    if previousValue != updatedValue {
                        if updatedValue {
                            command.isEnabled = true
                            command.addTarget(self, action: selector)
                        } else {
                            command.isEnabled = false
                            command.removeTarget(self, action: selector)
                        }
                    }
                }
                
                currentGlobalControlsOptions = updatedGlobalControlOptions
            }
        }))
        
        self.globalControlsArtworkDisposable.set((self.globalControlsArtwork.get()
        |> distinctUntilChanged(isEqual: { $0?.0 === $1?.0 && $0?.1 == $1?.1 })
        |> mapToSignal { value -> Signal<UIImage?, NoError> in
            if let (account, value) = value {
                return playerAlbumArt(postbox: account.postbox, engine: TelegramEngine(account: account), fileReference: value.fullSizeResource.file, albumArt: value, thumbnail: false)
                |> map { generator -> UIImage? in
                    let arguments = TransformImageArguments(corners: ImageCorners(), imageSize: CGSize(width: 640, height: 640), boundingSize: CGSize(width: 640, height: 640), intrinsicInsets: .zero)
                    return generator(arguments)?.generateImage()
                }
            } else {
                return .single(nil)
            }
        } |> deliverOnMainQueue).startStrict(next: { image in
            if var nowPlayingInfo = baseNowPlayingInfo {
                if let image = image {
                    if #available(iOSApplicationExtension 10.0, iOS 10.0, *) {
                        nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(boundsSize: image.size, requestHandler: { size in
                            return image
                        })
                    } else {
                        nowPlayingInfo[MPMediaItemPropertyArtwork] = MPMediaItemArtwork(image: image)
                    }
                } else {
                    nowPlayingInfo.removeValue(forKey: MPMediaItemPropertyArtwork)
                }
                MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
                baseNowPlayingInfo = nowPlayingInfo
            }
        }))
        
        self.globalControlsStatusDisposable.set((self.globalControlsStatus.get()
        |> deliverOnMainQueue).startStrict(next: { next in
            if let next = next {
                if var nowPlayingInfo = baseNowPlayingInfo {
                    nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = next.duration as NSNumber
                    switch next.status {
                        case .playing:
                            nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 1.0 as NSNumber
                        case .buffering, .paused:
                            nowPlayingInfo[MPNowPlayingInfoPropertyPlaybackRate] = 0.0 as NSNumber
                    }
                    nowPlayingInfo[MPNowPlayingInfoPropertyElapsedPlaybackTime] = next.timestamp as NSNumber
                    
                    MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
                }
            }
        }))
        
       
        let shouldKeepAudioSession: Signal<Bool, NoError> = combineLatest(queue: Queue.mainQueue(), self.globalMediaPlayerState, inForeground)
        |> map { stateAndType, inForeground -> Bool in
            var isPlaying = false
            if let (_, stateOrLoading, _) = stateAndType, case let .state(state) = stateOrLoading {
                switch state.status.status {
                    case .playing:
                        isPlaying = true
                    case let .buffering(_, whilePlaying, _, _):
                        isPlaying = whilePlaying
                    default:
                        break
                }
            }
            if !inForeground {
                if !isPlaying {
                    return true
                }
            }
            return false
        }
        |> distinctUntilChanged
        |> mapToSignal { value -> Signal<Bool, NoError> in
            if value {
                return .single(true) |> delay(0.8, queue: Queue.mainQueue())
            } else {
                return .single(false)
            }
        }
        
        let throttledSignal = self.globalMediaPlayerState
        |> mapToThrottled { next -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in
            return .single(next) |> then(.complete() |> delay(2.0, queue: Queue.concurrentDefaultQueue()))
        }
        
        self.mediaPlaybackStateDisposable.set(throttledSignal.startStrict(next: { accountStateAndType in
            let minimumStoreDuration: Double?
            if let (account, stateOrLoading, type) = accountStateAndType {
                switch type {
                    case .music:
                        minimumStoreDuration = 10.0 * 60.0
                    case .voice:
                        minimumStoreDuration = 5.0 * 60.0
                    case .file:
                        minimumStoreDuration = nil
                }
            
                if let minimumStoreDuration = minimumStoreDuration, case let .state(state) = stateOrLoading, state.status.duration >= minimumStoreDuration, case .playing = state.status.status {
                    if let item = state.item as? MessageMediaPlaylistItem {
                        var storedState: MediaPlaybackStoredState?
                        if state.status.timestamp > 5.0 && state.status.timestamp < state.status.duration - 5.0 {
                            storedState = MediaPlaybackStoredState(timestamp: state.status.timestamp, playbackRate: state.status.baseRate > 1.0 ? .x2 : .x1)
                        }
                        let _ = updateMediaPlaybackStoredStateInteractively(engine: TelegramEngine(account: account), messageId: item.message.id, state: storedState).startStandalone()
                    }
                }
            }
        }))
        
        self.globalAudioSessionForegroundDisposable.set((shouldKeepAudioSession |> deliverOnMainQueue).startStrict(next: { [weak self] value in
            guard let strongSelf = self else {
                return
            }
            if strongSelf.isCurrent && value {
                strongSelf.audioSession.dropAll()
            }
        }))
    }
    
    deinit {
        self.globalControlsDisposable.dispose()
        self.globalControlsArtworkDisposable.dispose()
        self.globalControlsStatusDisposable.dispose()
        self.setPlaylistByTypeDisposables.dispose()
        self.mediaPlaybackStateDisposable.dispose()
        self.globalAudioSessionForegroundDisposable.dispose()
        self.voiceMediaPlayerStateDisposable.dispose()
    }
    
    public func audioRecorder(beginWithTone: Bool, applicationBindings: TelegramApplicationBindings, beganWithTone: @escaping (Bool) -> Void) -> Signal<ManagedAudioRecorder?, NoError> {
        return Signal { subscriber in
            let disposable = MetaDisposable()
            
            self.queue.async {
                let audioRecorder = ManagedAudioRecorderImpl(mediaManager: self, pushIdleTimerExtension: { [weak applicationBindings] in
                    return applicationBindings?.pushIdleTimerExtension() ?? EmptyDisposable
                }, beginWithTone: beginWithTone, beganWithTone: beganWithTone)
                subscriber.putNext(audioRecorder)
                
                disposable.set(ActionDisposable {
                })
            }
            return disposable
        }
    }
    
    public func setPlaylist(_ playlist: (Account, SharedMediaPlaylist)?, type: MediaManagerPlayerType, control: SharedMediaPlayerControlAction) {
        assert(Queue.mainQueue().isCurrent())
        let inputData: Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings, MediaPlaybackStoredState?)?, NoError>
        if let (account, playlist) = playlist {
            inputData = self.accountManager.sharedData(keys: [ApplicationSpecificSharedDataKeys.musicPlaybackSettings])
            |> take(1)
            |> mapToSignal { sharedData -> Signal<(Account, SharedMediaPlaylist, MusicPlaybackSettings, MediaPlaybackStoredState?)?, NoError> in
                let settings = sharedData.entries[ApplicationSpecificSharedDataKeys.musicPlaybackSettings]?.get(MusicPlaybackSettings.self) ?? MusicPlaybackSettings.defaultSettings
                
                if let location = playlist.location as? PeerMessagesPlaylistLocation, let messageId = location.messageId {
                    return mediaPlaybackStoredState(engine: TelegramEngine(account: account), messageId: messageId)
                    |> map { storedState in
                        return (account, playlist, settings, storedState)
                    }
                } else {
                    return .single((account, playlist, settings, nil))
                }
            }
        } else {
            inputData = .single(nil)
        }
        
        self.setPlaylistByTypeDisposables.set((inputData
        |> deliverOnMainQueue).startStrict(next: { [weak self] inputData in
            if let strongSelf = self {
                let nextPlayerIndex = strongSelf.nextPlayerIndex
                strongSelf.nextPlayerIndex += 1
                switch type {
                    case .voice:
                        strongSelf.musicMediaPlayer?.control(.playback(.pause))
                        if let (account, playlist, settings, storedState) = inputData {
                            if areSharedMediaPlaylistsEqual(playlist, strongSelf.voiceMediaPlayer?.playlist), case .seek = control {
                                strongSelf.voiceMediaPlayer?.control(control)
                            } else {
                                strongSelf.voiceMediaPlayer?.stop()
                                var continueInstantVideoLoopAfterFinish: Bool = true
                                var controlPlaybackWithProximity: Bool = true
                                if let playlist = playlist as? PeerMessagesMediaPlaylist {
                                    continueInstantVideoLoopAfterFinish = playlist.context.sharedContext.energyUsageSettings.autoplayVideo
                                    controlPlaybackWithProximity = playlist.context.sharedContext.currentMediaInputSettings.with({ $0.enableRaiseToSpeak })
                                }
                                
                                let voiceMediaPlayer = SharedMediaPlayer(mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: .reversed, initialLooping: .none, initialPlaybackRate: settings.voicePlaybackRate, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: controlPlaybackWithProximity, type: type, continueInstantVideoLoopAfterFinish: continueInstantVideoLoopAfterFinish)
                                strongSelf.voiceMediaPlayer = voiceMediaPlayer
                                voiceMediaPlayer.playedToEnd = { [weak voiceMediaPlayer] in
                                    if let strongSelf = self, let voiceMediaPlayer = voiceMediaPlayer, voiceMediaPlayer === strongSelf.voiceMediaPlayer {
                                        voiceMediaPlayer.stop()
                                        strongSelf.voiceMediaPlayer = nil
                                    }
                                }
                                voiceMediaPlayer.cancelled = { [weak voiceMediaPlayer] in
                                    if let strongSelf = self, let voiceMediaPlayer = voiceMediaPlayer, voiceMediaPlayer === strongSelf.voiceMediaPlayer {
                                        voiceMediaPlayer.stop()
                                        strongSelf.voiceMediaPlayer = nil
                                    }
                                }
                                
                                var control = control
                                if case .seek = control {
                                } else if let timestamp = storedState?.timestamp {
                                    control = .seek(timestamp)
                                }
                                voiceMediaPlayer.control(control)
                                voiceMediaPlayer.control(.playback(.play))
                            }
                        } else {
                            strongSelf.voiceMediaPlayer?.stop()
                            strongSelf.voiceMediaPlayer = nil
                        }
                    case .music, .file:
                        strongSelf.voiceMediaPlayer?.control(.playback(.pause))
                        if let (account, playlist, settings, storedState) = inputData {
                            if areSharedMediaPlaylistsEqual(playlist, strongSelf.musicMediaPlayer?.playlist), case .seek = control {
                                strongSelf.musicMediaPlayer?.control(control)
                            } else {
                                strongSelf.musicMediaPlayer?.stop()
                                let musicMediaPlayer = SharedMediaPlayer(mediaManager: strongSelf, inForeground: strongSelf.inForeground, account: account, audioSession: strongSelf.audioSession, overlayMediaManager: strongSelf.overlayMediaManager, playlist: playlist, initialOrder: settings.order, initialLooping: settings.looping, initialPlaybackRate: storedState?.playbackRate ?? .x1, playerIndex: nextPlayerIndex, controlPlaybackWithProximity: false, type: type, continueInstantVideoLoopAfterFinish: true)
                                strongSelf.musicMediaPlayer = musicMediaPlayer
                                musicMediaPlayer.cancelled = { [weak musicMediaPlayer] in
                                    if let strongSelf = self, let musicMediaPlayer = musicMediaPlayer, musicMediaPlayer === strongSelf.musicMediaPlayer {
                                        musicMediaPlayer.stop()
                                        strongSelf.musicMediaPlayer = nil
                                    }
                                }
                                
                                var control = control
                                if case .seek = control {
                                } else if let timestamp = storedState?.timestamp {
                                    control = .seek(timestamp)
                                }
                                strongSelf.musicMediaPlayer?.control(control)
                                strongSelf.musicMediaPlayer?.control(.playback(.play))
                            }
                        } else {
                            strongSelf.musicMediaPlayer?.stop()
                            strongSelf.musicMediaPlayer = nil
                        }
                }
            }
        }), forKey: type)
    }
    
    public func playlistControl(_ control: SharedMediaPlayerControlAction, type: MediaManagerPlayerType?) {
        assert(Queue.mainQueue().isCurrent())
        let selectedType: MediaManagerPlayerType
        if let type = type {
            selectedType = type
        } else if self.voiceMediaPlayer != nil {
            selectedType = .voice
        } else {
            selectedType = .music
        }
        switch selectedType {
            case .voice:
                self.voiceMediaPlayer?.control(control)
            case .music, .file:
                if self.voiceMediaPlayer != nil {
                    switch control {
                        case .playback(.play), .playback(.togglePlayPause):
                            self.setPlaylist(nil, type: .voice, control: .playback(.pause))
                        default:
                            break
                    }
                }
                self.musicMediaPlayer?.control(control)
        }
    }
    
    public func filteredPlaylistState(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal<SharedMediaPlayerItemPlaybackState?, NoError> {
        let signal: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading)?, NoError>
        switch type {
            case .voice:
                signal = self.voiceMediaPlayerState
            case .music, .file:
                signal = self.musicMediaPlayerState
                |> map { value in
                    return value.flatMap { ($0.0, $0.1) }
                }
        }
        return signal
        |> map { stateOrLoading -> SharedMediaPlayerItemPlaybackState? in
            if let (account, stateOrLoading) = stateOrLoading, account.id == accountId, case let .state(state) = stateOrLoading {
                if state.playlistId.isEqual(to: playlistId) && state.item.id.isEqual(to: itemId) {
                    return state
                }
            }
            return nil
        } |> distinctUntilChanged(isEqual: { lhs, rhs in
            return lhs == rhs
        })
    }
    
    public func filteredPlayerAudioLevelEvents(accountId: AccountRecordId, playlistId: SharedMediaPlaylistId, itemId: SharedMediaPlaylistItemId, type: MediaManagerPlayerType) -> Signal<Float, NoError> {
        switch type {
            case .voice:
                return self.voiceMediaPlayerAudioLevelEvents.get()
                |> mapToSignal { value -> Signal<Float, NoError> in
                    guard let value = value else {
                        return .never()
                    }
                    let (accountIdValue, playlistIdValue, itemIdValue, signal) = value
                    if accountIdValue == accountId && playlistId.isEqual(to: playlistIdValue) && itemId.isEqual(to: itemIdValue) {
                        return signal
                    } else {
                        return .never()
                    }
                }
            case .music, .file:
                return .never()
        }
    }
    
    @objc func playCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus {
        self.playlistControl(.playback(.play), type: nil)
        
        return .success
    }
    
    @objc func pauseCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus {
        self.playlistControl(.playback(.pause), type: nil)
        
        return .success
    }
    
    @objc func previousTrackCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus {
        self.playlistControl(.previous, type: nil)
        
        return .success
    }
    
    @objc func nextTrackCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus {
        self.playlistControl(.next, type: nil)
        
        return .success
    }
    
    @objc func togglePlayPauseCommandEvent(_ command: AnyObject) -> MPRemoteCommandHandlerStatus {
        self.playlistControl(.playback(.togglePlayPause), type: nil)
        
        return .success
    }
    
    @objc func changePlaybackPositionCommandEvent(_ event: MPChangePlaybackPositionCommandEvent) -> MPRemoteCommandHandlerStatus {
        self.playlistControl(.seek(event.positionTime), type: nil)
        
        return .success
    }
    
    public func setOverlayVideoNode(_ node: OverlayMediaItemNode?) {
        if let currentOverlayVideoNode = self.currentOverlayVideoNode {
            self.overlayMediaManager.controller?.removeNode(currentOverlayVideoNode, customTransition: true)
            self.currentOverlayVideoNode = nil
        }
        
        if let node = node {
            self.currentOverlayVideoNode = node
            self.overlayMediaManager.controller?.addNode(node, customTransition: true)
        }
    }
    
    public func hasOverlayVideoNode(_ node: OverlayMediaItemNode) -> Bool {
        return self.currentOverlayVideoNode === node
    }
}