import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
import UniversalMediaPlayer
import TelegramUIPreferences
import AccountContext
import PhotoResources
import AppBundle
import ManagedAnimationNode
import RangeSet
import TelegramBaseController
import ContextUI
import SliderContextItem
import UndoUI

private func normalizeValue(_ value: CGFloat) -> CGFloat {
    return round(value * 10.0) / 10.0
}

private func generateBackground(theme: PresentationTheme) -> UIImage? {
    return generateImage(CGSize(width: 20.0, height: 10.0 + 8.0), rotatedContext: { size, context in
        context.clear(CGRect(origin: CGPoint(), size: size))
        context.setShadow(offset: CGSize(width: 0.0, height: -4.0), blur: 20.0, color: UIColor(white: 0.0, alpha: 0.3).cgColor)
        context.setFillColor(theme.list.plainBackgroundColor.cgColor)
        context.fillEllipse(in: CGRect(origin: CGPoint(x: 0.0, y: 8.0), size: CGSize(width: 20.0, height: 20.0)))
    })?.stretchableImage(withLeftCapWidth: 10, topCapHeight: 10 + 8)
}

private func generateCollapseIcon(theme: PresentationTheme) -> UIImage? {
    return generateImage(CGSize(width: 38.0, height: 5.0), rotatedContext: { size, context in
        let bounds = CGRect(origin: CGPoint(), size: size)
        context.clear(bounds)
        
        let path = UIBezierPath(roundedRect: bounds, cornerRadius: 2.5)
        context.setFillColor(theme.list.controlSecondaryColor.cgColor)
        context.addPath(path.cgPath)
        context.fillPath()
    })
}

private func optionsRateImage(rate: String, color: UIColor = .white) -> UIImage? {
    let isLarge = "".isEmpty
    return generateImage(isLarge ? CGSize(width: 30.0, height: 30.0) : CGSize(width: 24.0, height: 24.0), rotatedContext: { size, context in
        UIGraphicsPushContext(context)

        context.clear(CGRect(origin: CGPoint(), size: size))

        if let image = generateTintedImage(image: UIImage(bundleImageName: isLarge ? "Chat/Context Menu/Playspeed30" : "Chat/Context Menu/Playspeed24"), color: color) {
            image.draw(at: CGPoint(x: 0.0, y: 0.0))
        }

        let string = NSMutableAttributedString(string: rate, font: Font.with(size: isLarge ? 11.0 : 10.0, design: .round, weight: .semibold), textColor: color)

        var offset = CGPoint(x: 1.0, y: 0.0)
        if rate.count >= 3 {
            if rate == "0.5x" {
                string.addAttribute(.kern, value: -0.8 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
                offset.x += -0.5
            } else {
                string.addAttribute(.kern, value: -0.5 as NSNumber, range: NSRange(string.string.startIndex ..< string.string.endIndex, in: string.string))
                offset.x += -0.3
            }
        } else {
            offset.x += -0.3
        }

        if !isLarge {
            offset.x *= 0.5
            offset.y *= 0.5
        }

        let boundingRect = string.boundingRect(with: size, options: [], context: nil)
        string.draw(at: CGPoint(x: offset.x + floor((size.width - boundingRect.width) / 2.0), y: offset.y + floor((size.height - boundingRect.height) / 2.0)))

        UIGraphicsPopContext()
    })
}

private let digitsSet = CharacterSet(charactersIn: "0123456789")
private func timestampLabelWidthForDuration(_ timestamp: Double) -> CGFloat {
    let text: String
    if timestamp > 0 {
        let timestamp = Int32(timestamp)
        let hours = timestamp / (60 * 60)
        let minutes = timestamp % (60 * 60) / 60
        let seconds = timestamp % 60
        if hours != 0 {
            text = String(format: "%d:%02d:%02d", hours, minutes, seconds)
        } else {
            text = String(format: "%d:%02d", minutes, seconds)
        }
    } else {
        text = "-:--"
    }
    
    let convertedString = text.components(separatedBy: digitsSet).joined(separator: "8")
    let string = NSAttributedString(string: convertedString, font: Font.regular(13.0), textColor: .black)
    let size = string.boundingRect(with: CGSize(width: 200.0, height: 100.0), options: NSStringDrawingOptions.usesLineFragmentOrigin, context: nil).size
    return size.width
}

private let titleFont = Font.semibold(18.0)
private let descriptionFont = Font.regular(18.0)

private func stringsForDisplayData(_ data: SharedMediaPlaybackDisplayData?, presentationData: PresentationData) -> (NSAttributedString?, NSAttributedString?, Bool, NSAttributedString?) {
    var titleString: NSAttributedString?
    var descriptionString: NSAttributedString?
    var hasArtist = false
    var captionString: NSAttributedString?
    
    if let data = data {
        let titleText: String
        let subtitleText: String
        switch data {
            case let .music(title, performer, _, _, caption):
                titleText = title ?? presentationData.strings.MediaPlayer_UnknownTrack
                subtitleText = performer ?? presentationData.strings.MediaPlayer_UnknownArtist
                hasArtist = performer != nil
                captionString = caption
            case .voice, .instantVideo:
                titleText = ""
                subtitleText = ""
        }
        
        titleString = NSAttributedString(string: titleText, font: titleFont, textColor: presentationData.theme.list.itemPrimaryTextColor)
        descriptionString = NSAttributedString(string: subtitleText, font: descriptionFont, textColor: hasArtist ? presentationData.theme.list.itemAccentColor : presentationData.theme.list.itemSecondaryTextColor)
    }
    
    return (titleString, descriptionString, hasArtist, captionString)
}

final class OverlayPlayerControlsNode: ASDisplayNode {
    private let accountManager: AccountManager<TelegramAccountManagerTypes>
    private let account: Account
    private let engine: TelegramEngine
    private var presentationData: PresentationData
    
    private let backgroundNode: ASImageNode
    
    private let collapseNode: HighlightableButtonNode
    
    private let albumArtNode: TransformImageNode
    private var largeAlbumArtNode: TransformImageNode?
    private let titleNode: TextNode
    private let descriptionNode: TextNode
    private let shareNode: HighlightableButtonNode
    private let artistButton: HighlightTrackingButtonNode
    
    private let scrubberNode: MediaPlayerScrubbingNode
    private let leftDurationLabel: MediaPlayerTimeTextNode
    private let rightDurationLabel: MediaPlayerTimeTextNode
    private let infoNode: ASTextNode
    
    private let backwardButton: IconButtonNode
    private let forwardButton: IconButtonNode
    
    private var seekTimer: SwiftSignalKit.Timer?
    private var seekRate: AudioPlaybackRate = .x2
    private var previousRate: AudioPlaybackRate?
    
    private var currentIsPaused: Bool?
    private let playPauseButton: IconButtonNode
    private let playPauseIconNode: PlayPauseIconNode
    
    private var currentOrder: MusicPlaybackSettingsOrder?
    private let orderButton: IconButtonNode
    
    private var currentLooping: MusicPlaybackSettingsLooping?
    private let loopingButton: IconButtonNode
    
    private var currentRate: AudioPlaybackRate?
    private let rateButton: AudioRateButton
    
    let separatorNode: ASDisplayNode
    
    var isExpanded = false
    var updateIsExpanded: (() -> Void)?
    
    var requestCollapse: (() -> Void)?
    var requestShare: ((MessageId) -> Void)?
    var requestSearchByArtist: ((String) -> Void)?
    
    var updateOrder: ((MusicPlaybackSettingsOrder) -> Void)?
    var control: ((SharedMediaPlayerControlAction) -> Void)?
    
    var getParentController: () -> ViewController? = { return nil }
    
    private(set) var currentItemId: SharedMediaPlaylistItemId?
    private var displayData: SharedMediaPlaybackDisplayData?
    private var currentAlbumArtInitialized = false
    private var currentAlbumArt: SharedMediaPlaybackAlbumArt?
    private var currentFileReference: FileMediaReference?
    private var statusDisposable: Disposable?
    private var chapterDisposable: Disposable?
    
    private var previousCaption: NSAttributedString?
    private var chaptersPromise = ValuePromise<[MediaPlayerScrubbingChapter]>([])
    private var currentChapter: MediaPlayerScrubbingChapter?
    
    private let hapticFeedback = HapticFeedback()
    
    private var scrubbingDisposable: Disposable?
    private var leftDurationLabelPushed = false
    private var rightDurationLabelPushed = false
    private var infoNodePushed = false
    
    private var currentDuration: Double = 0.0
    private var currentPosition: Double = 0.0
    
    private var validLayout: (width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat)?
    
    init(account: Account, engine: TelegramEngine, accountManager: AccountManager<TelegramAccountManagerTypes>, presentationData: PresentationData, status: Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError>) {
        self.accountManager = accountManager
        self.account = account
        self.engine = engine
        self.presentationData = presentationData
        
        self.backgroundNode = ASImageNode()
        self.backgroundNode.isLayerBacked = true
        self.backgroundNode.displayWithoutProcessing = true
        self.backgroundNode.displaysAsynchronously = false
        self.backgroundNode.image = generateBackground(theme: presentationData.theme)
        
        self.collapseNode = HighlightableButtonNode()
        self.collapseNode.displaysAsynchronously = false
        self.collapseNode.setImage(generateCollapseIcon(theme: presentationData.theme), for: [])
        
        self.albumArtNode = TransformImageNode()
        
        self.titleNode = TextNode()
        self.titleNode.isUserInteractionEnabled = false
        self.titleNode.displaysAsynchronously = false
        
        self.descriptionNode = TextNode()
        self.descriptionNode.isUserInteractionEnabled = false
        self.descriptionNode.displaysAsynchronously = false
        
        self.artistButton = HighlightTrackingButtonNode()
        
        self.shareNode = HighlightableButtonNode()
        self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: presentationData.theme.list.itemAccentColor), for: [])
        
        self.scrubberNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: presentationData.theme.list.controlSecondaryColor, foregroundColor: presentationData.theme.list.itemAccentColor, bufferingColor: presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), chapters: []))
        self.leftDurationLabel = MediaPlayerTimeTextNode(textColor: presentationData.theme.list.itemSecondaryTextColor)
        self.leftDurationLabel.displaysAsynchronously = false
        self.leftDurationLabel.keepPreviousValueOnEmptyState = true
        self.rightDurationLabel = MediaPlayerTimeTextNode(textColor: presentationData.theme.list.itemSecondaryTextColor)
        self.rightDurationLabel.displaysAsynchronously = false
        self.rightDurationLabel.mode = .reversed
        self.rightDurationLabel.alignment = .right
        self.rightDurationLabel.keepPreviousValueOnEmptyState = true
        
        self.infoNode = ASTextNode()
        self.infoNode.maximumNumberOfLines = 1
        self.infoNode.isUserInteractionEnabled = false
        self.infoNode.displaysAsynchronously = false
        
        self.rateButton = AudioRateButton()
        self.rateButton.hitTestSlop = UIEdgeInsets(top: -8.0, left: -4.0, bottom: -8.0, right: -4.0)
        self.rateButton.displaysAsynchronously = false
        
        self.backwardButton = IconButtonNode()
        self.backwardButton.displaysAsynchronously = false
        
        self.forwardButton = IconButtonNode()
        self.forwardButton.displaysAsynchronously = false
        
        self.orderButton = IconButtonNode()
        self.orderButton.displaysAsynchronously = false
        
        self.loopingButton = IconButtonNode()
        self.loopingButton.displaysAsynchronously = false
        
        self.playPauseButton = IconButtonNode()
        self.playPauseButton.displaysAsynchronously = false
        
        self.playPauseIconNode = PlayPauseIconNode()
        
        self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor)
        self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor)
        
        self.separatorNode = ASDisplayNode()
        self.separatorNode.isLayerBacked = true
        self.separatorNode.backgroundColor = presentationData.theme.list.itemPlainSeparatorColor
        
        super.init()
        
        self.addSubnode(self.backgroundNode)
        
        self.addSubnode(self.collapseNode)
        
        self.addSubnode(self.albumArtNode)
        self.addSubnode(self.titleNode)
        self.addSubnode(self.descriptionNode)
        self.addSubnode(self.artistButton)
        self.addSubnode(self.shareNode)
        
        self.addSubnode(self.leftDurationLabel)
        self.addSubnode(self.rightDurationLabel)
        self.addSubnode(self.infoNode)
        self.addSubnode(self.rateButton)
        self.addSubnode(self.scrubberNode)
        
        self.addSubnode(self.orderButton)
        self.addSubnode(self.loopingButton)
        self.addSubnode(self.backwardButton)
        self.addSubnode(self.forwardButton)
        self.addSubnode(self.playPauseButton)
        self.playPauseButton.addSubnode(self.playPauseIconNode)
        
        self.addSubnode(self.separatorNode)
        
        let accountId = account.id
        let delayedStatus = status
        |> mapToSignal { value -> Signal<(Account, SharedMediaPlayerItemPlaybackStateOrLoading, MediaManagerPlayerType)?, NoError> in
            guard let value = value, value.0.id == accountId else {
                return .single(nil)
            }
            switch value.1 {
                case .state:
                    return .single(value)
                case .loading:
                    return .single(value)
                    |> delay(0.1, queue: .mainQueue())
            }
        }
        
        let mappedStatus = combineLatest(delayedStatus, self.scrubberNode.scrubbingTimestamp) |> map { value, scrubbingTimestamp -> MediaPlayerStatus in
            if let (_, valueOrLoading, _) = value, case let .state(value) = valueOrLoading {
                return MediaPlayerStatus(generationTimestamp: scrubbingTimestamp != nil ? 0 : value.status.generationTimestamp, duration: value.status.duration, dimensions: value.status.dimensions, timestamp: scrubbingTimestamp ?? value.status.timestamp, baseRate: value.status.baseRate, seekId: value.status.seekId, status: value.status.status, soundEnabled: value.status.soundEnabled)
            } else {
                return MediaPlayerStatus(generationTimestamp: 0.0, duration: 0.0, dimensions: CGSize(), timestamp: 0.0, baseRate: 1.0, seekId: 0, status: .paused, soundEnabled: true)
            }
        }
        self.scrubberNode.status = mappedStatus
        self.leftDurationLabel.status = mappedStatus
        self.rightDurationLabel.status = mappedStatus
        
        self.scrubbingDisposable = (self.scrubberNode.scrubbingPosition
        |> deliverOnMainQueue).startStrict(next: { [weak self] value in
            guard let strongSelf = self else {
                return
            }
            let leftDurationLabelPushed: Bool
            let rightDurationLabelPushed: Bool
            let infoNodePushed: Bool
            if let value = value {
                leftDurationLabelPushed = value < 0.16
                rightDurationLabelPushed = value > (strongSelf.rateButton.isHidden ? 0.84 : 0.74)
                infoNodePushed = value >= 0.16 && value <= 0.84
            } else {
                leftDurationLabelPushed = false
                rightDurationLabelPushed = false
                infoNodePushed = false
            }
            if leftDurationLabelPushed != strongSelf.leftDurationLabelPushed || rightDurationLabelPushed != strongSelf.rightDurationLabelPushed || infoNodePushed != strongSelf.infoNodePushed {
                strongSelf.leftDurationLabelPushed = leftDurationLabelPushed
                strongSelf.rightDurationLabelPushed = rightDurationLabelPushed
                strongSelf.infoNodePushed = infoNodePushed
                
                if let layout = strongSelf.validLayout {
                    let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .animated(duration: 0.35, curve: .spring))
                }
            }
        })
        
        self.statusDisposable = (delayedStatus
        |> deliverOnMainQueue).startStrict(next: { [weak self] value in
            guard let strongSelf = self else {
                return
            }
            var valueItemId: SharedMediaPlaylistItemId?
            if let (_, value, _) = value, case let .state(state) = value {
                valueItemId = state.item.id
            }
            if !areSharedMediaPlaylistItemIdsEqual(valueItemId, strongSelf.currentItemId) {
                strongSelf.currentItemId = valueItemId
                strongSelf.scrubberNode.ignoreSeekId = nil
            }
            
            var rateButtonIsHidden = true
            var displayData: SharedMediaPlaybackDisplayData?
            if let (_, valueOrLoading, _) = value, case let .state(value) = valueOrLoading {
                var isPaused: Bool
                switch value.status.status {
                    case .playing:
                        isPaused = false
                    case .paused:
                        isPaused = true
                    case let .buffering(_, whilePlaying, _, _):
                        isPaused = !whilePlaying
                }
                if strongSelf.wasPlaying {
                    isPaused = false
                }
                
                let isFirstTime = strongSelf.currentIsPaused == nil
                if strongSelf.currentIsPaused != isPaused {
                    strongSelf.currentIsPaused = isPaused
                    
                    strongSelf.updatePlayPauseButton(paused: isPaused, animated: !isFirstTime)
                }
                
                strongSelf.playPauseButton.isEnabled = true
                strongSelf.backwardButton.isEnabled = true
                strongSelf.forwardButton.isEnabled = true
                
                displayData = value.item.displayData
                
                if value.order != strongSelf.currentOrder {
                    strongSelf.updateOrder?(value.order)
                    strongSelf.currentOrder = value.order
                    strongSelf.updateOrderButton(value.order)
                }
                if value.looping != strongSelf.currentLooping {
                    strongSelf.currentLooping = value.looping
                    strongSelf.updateLoopButton(value.looping)
                }
                
                let baseRate = AudioPlaybackRate(value.status.baseRate )
                if baseRate != strongSelf.currentRate {
                    strongSelf.currentRate = baseRate
                    strongSelf.updateRateButton(baseRate)
                }
                
                if let displayData = displayData, case let .music(_, _, _, long, _) = displayData, long {
                    strongSelf.scrubberNode.enableFineScrubbing = true
                    rateButtonIsHidden = false
                } else {
                    strongSelf.scrubberNode.enableFineScrubbing = false
                    rateButtonIsHidden = true
                }
                
                let duration = value.status.duration
                if duration != strongSelf.currentDuration && !duration.isZero {
                    strongSelf.currentDuration = duration
                    if let layout = strongSelf.validLayout {
                        let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .immediate)
                    }
                }
                
                strongSelf.rateButton.isHidden = rateButtonIsHidden
                
                strongSelf.currentPosition = value.status.timestamp
            } else {
                strongSelf.playPauseButton.isEnabled = false
                strongSelf.backwardButton.isEnabled = false
                strongSelf.forwardButton.isEnabled = false
                strongSelf.rateButton.isHidden = true
                displayData = nil
            }
            
            if strongSelf.displayData != displayData {
                strongSelf.displayData = displayData
                          
                var canShare = true
                if let (_, valueOrLoading, _) = value, case let .state(value) = valueOrLoading, let source = value.item.playbackData?.source {
                    switch source {
                        case let .telegramFile(fileReference, isCopyProtected, _):
                            canShare = !isCopyProtected
                            strongSelf.currentFileReference = fileReference
                            if let size = fileReference.media.size {
                                strongSelf.scrubberNode.bufferingStatus = strongSelf.account.postbox.mediaBox.resourceRangesStatus(fileReference.media.resource)
                                |> map { ranges -> (RangeSet<Int64>, Int64) in
                                    return (ranges, size)
                                }
                            } else {
                                strongSelf.scrubberNode.bufferingStatus = nil
                            }
                    }
                } else {
                    strongSelf.scrubberNode.bufferingStatus = nil
                }
                strongSelf.updateLabels(transition: .immediate)
                
                strongSelf.shareNode.isHidden = !canShare
            }
        })
        
        self.chapterDisposable = combineLatest(queue: Queue.mainQueue(), mappedStatus, self.chaptersPromise.get())
        .startStrict(next: { [weak self] status, chapters in
            if let strongSelf = self, status.duration > 1.0, chapters.count > 0 {
                let previousChapter = strongSelf.currentChapter
                var currentChapter: MediaPlayerScrubbingChapter?
                for chapter in chapters {
                    if chapter.start > status.timestamp {
                        break
                    } else {
                        currentChapter = chapter
                    }
                }
                
                if let chapter = currentChapter, chapter != previousChapter {
                    strongSelf.currentChapter = chapter
                    
                    if strongSelf.scrubberNode.isScrubbing {
                        strongSelf.hapticFeedback.impact(.light)
                    }
                    
                    if let previousChapter = previousChapter, !strongSelf.infoNode.alpha.isZero {
                        if let snapshotView = strongSelf.infoNode.view.snapshotView(afterScreenUpdates: false) {
                            snapshotView.frame = strongSelf.infoNode.frame
                            strongSelf.infoNode.view.superview?.addSubview(snapshotView)
                            
                            let offset: CGFloat = 30.0
                            let snapshotTargetPosition: CGPoint
                            let nodeStartPosition: CGPoint
                            if previousChapter.start < chapter.start {
                                snapshotTargetPosition = CGPoint(x: -offset, y: 0.0)
                                nodeStartPosition = CGPoint(x: offset, y: 0.0)
                            } else {
                                snapshotTargetPosition = CGPoint(x: offset, y: 0.0)
                                nodeStartPosition = CGPoint(x: -offset, y: 0.0)
                            }
                            snapshotView.layer.animatePosition(from: CGPoint(), to: snapshotTargetPosition, duration: 0.2, removeOnCompletion: false, additive: true)
                            snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
                                snapshotView?.removeFromSuperview()
                            })
                            strongSelf.infoNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                            strongSelf.infoNode.layer.animatePosition(from: nodeStartPosition, to: CGPoint(), duration: 0.2, additive: true)
                        }
                    }
                    strongSelf.infoNode.attributedText = NSAttributedString(string: chapter.title, font: Font.regular(13.0), textColor: strongSelf.presentationData.theme.list.itemSecondaryTextColor)
                    
                    if let layout = strongSelf.validLayout {
                        let _ = strongSelf.updateLayout(width: layout.0, leftInset: layout.1, rightInset: layout.2, maxHeight: layout.3, transition: .immediate)
                    }
                }
            }
        })
        
        self.scrubberNode.seek = { [weak self] value in
            self?.control?(.seek(value))
        }
        
        self.collapseNode.addTarget(self, action: #selector(self.collapsePressed), forControlEvents: .touchUpInside)
        self.shareNode.addTarget(self, action: #selector(self.sharePressed), forControlEvents: .touchUpInside)
        self.orderButton.addTarget(self, action: #selector(self.orderPressed), forControlEvents: .touchUpInside)
        self.loopingButton.addTarget(self, action: #selector(self.loopingPressed), forControlEvents: .touchUpInside)
        self.backwardButton.addTarget(self, action: #selector(self.backwardPressed), forControlEvents: .touchUpInside)
        self.forwardButton.addTarget(self, action: #selector(self.forwardPressed), forControlEvents: .touchUpInside)
        self.playPauseButton.addTarget(self, action: #selector(self.playPausePressed), forControlEvents: .touchUpInside)
        self.rateButton.addTarget(self, action: #selector(self.rateButtonPressed), forControlEvents: .touchUpInside)
        self.artistButton.addTarget(self, action: #selector(self.artistPressed), forControlEvents: .touchUpInside)
        
        self.artistButton.highligthedChanged = { [weak self] highlighted in
            if let strongSelf = self {
                if highlighted {
                    strongSelf.descriptionNode.layer.removeAnimation(forKey: "opacity")
                    strongSelf.descriptionNode.alpha = 0.4
                } else {
                    strongSelf.descriptionNode.alpha = 1.0
                    strongSelf.descriptionNode.layer.animateAlpha(from: 0.4, to: 1.0, duration: 0.2)
                }
            }
        }
        
        self.rateButton.contextAction = { [weak self] sourceNode, gesture in
            self?.openRateMenu(sourceNode: sourceNode, gesture: gesture)
        }
        
        self.playPauseButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
        self.backwardButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
        self.forwardButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
    }
    
    deinit {
        self.statusDisposable?.dispose()
        self.chapterDisposable?.dispose()
        self.scrubbingDisposable?.dispose()
    }
    
    override func didLoad() {
        super.didLoad()
        
        self.albumArtNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTap(_:))))
        
        let backwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekBackwardLongPress(_:)))
        backwardLongPressGestureRecognizer.minimumPressDuration = 0.3
        self.backwardButton.view.addGestureRecognizer(backwardLongPressGestureRecognizer)
        
        let forwardLongPressGestureRecognizer = UILongPressGestureRecognizer(target: self, action: #selector(self.seekForwardLongPress(_:)))
        forwardLongPressGestureRecognizer.minimumPressDuration = 0.3
        self.forwardButton.view.addGestureRecognizer(forwardLongPressGestureRecognizer)
    }
    
    private var wasPlaying = false
    @objc private func seekBackwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
        switch gestureRecognizer.state {
            case .began:
                self.wasPlaying = !(self.currentIsPaused ?? true)
                self.backwardButton.isPressing = true
                self.previousRate = self.currentRate
                self.control?(.playback(.pause))
                
                var time: Double = 0.0
                let seekTimer = SwiftSignalKit.Timer(timeout: 0.1, repeat: true, completion: { [weak self] in
                    if let strongSelf = self {
                        var delta: Double = 0.8
                        if time >= 4.0 {
                            delta = 3.2
                        } else if time >= 2.0 {
                            delta = 1.6
                        }
                        time += 0.1
                        
                        let newPosition = strongSelf.currentPosition - delta
                        strongSelf.currentPosition = newPosition
                        strongSelf.control?(.seek(newPosition))
                    }
                }, queue: Queue.mainQueue())
                self.seekTimer = seekTimer
                seekTimer.start()
            case .ended, .cancelled:
                self.backwardButton.isPressing = false
                self.seekTimer?.invalidate()
                self.seekTimer = nil
                if self.wasPlaying {
                    self.control?(.playback(.play))
                }
                self.previousRate = nil
            default:
                break
        }
    }
    
    @objc private func seekForwardLongPress(_ gestureRecognizer: UILongPressGestureRecognizer) {
        switch gestureRecognizer.state {
            case .began:
                self.wasPlaying = !(self.currentIsPaused ?? true)
                self.forwardButton.isPressing = true
                self.previousRate = self.currentRate
                self.seekRate = .x4
                self.control?(.playback(.play))
                self.control?(.setBaseRate(self.seekRate))
                let seekTimer = SwiftSignalKit.Timer(timeout: 2.0, repeat: true, completion: { [weak self] in
                    if let strongSelf = self {
                        if strongSelf.seekRate == .x4 {
                            strongSelf.seekRate = .x8
                        } else if strongSelf.seekRate == .x8 {
                            strongSelf.seekRate = .x16
                        }
                        strongSelf.control?(.setBaseRate(strongSelf.seekRate))
                        if strongSelf.seekRate == .x16 {
                            strongSelf.seekTimer?.invalidate()
                            strongSelf.seekTimer = nil
                        }
                    }
                }, queue: Queue.mainQueue())
                self.seekTimer = seekTimer
                seekTimer.start()
            case .ended, .cancelled:
                self.forwardButton.isPressing = false
                self.control?(.setBaseRate(self.previousRate ?? .x1))
                self.seekTimer?.invalidate()
                self.seekTimer = nil
                if !self.wasPlaying {
                    self.control?(.playback(.pause))
                }
                self.previousRate = nil
            default:
                break
        }
    }
    
    func updatePresentationData(_ presentationData: PresentationData) {
        guard self.presentationData.theme !== presentationData.theme else {
            return
        }
        self.presentationData = presentationData
        
        self.playPauseButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
        self.backwardButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
        self.forwardButton.circleColor = presentationData.theme.list.controlSecondaryColor.withAlphaComponent(0.35)
        
        self.backgroundNode.image = generateBackground(theme: presentationData.theme)
        self.collapseNode.setImage(generateCollapseIcon(theme: presentationData.theme), for: [])
        self.shareNode.setImage(generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Share"), color: presentationData.theme.list.itemAccentColor), for: [])
        self.scrubberNode.updateColors(backgroundColor: presentationData.theme.list.controlSecondaryColor, foregroundColor: presentationData.theme.list.itemAccentColor)
        self.leftDurationLabel.textColor = presentationData.theme.list.itemSecondaryTextColor
        self.rightDurationLabel.textColor = presentationData.theme.list.itemSecondaryTextColor
        self.backwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Previous"), color: presentationData.theme.list.itemPrimaryTextColor)
        self.forwardButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Next"), color: presentationData.theme.list.itemPrimaryTextColor)
        if let isPaused = self.currentIsPaused {
            self.updatePlayPauseButton(paused: isPaused, animated: false)
        }
        if let order = self.currentOrder {
            self.updateOrderButton(order)
        }
        if let looping = self.currentLooping {
            self.updateLoopButton(looping)
        }
        if let rate = self.currentRate {
            self.updateRateButton(rate)
        }
        self.separatorNode.backgroundColor = presentationData.theme.list.itemPlainSeparatorColor
    }
    
    private func updateLabels(transition: ContainedViewLayoutTransition) {
        guard let (width, leftInset, rightInset, maxHeight) = self.validLayout else {
            return
        }
        
        let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded)
        
        let sideInset: CGFloat = 20.0
        
        let infoLabelsLeftInset: CGFloat = 60.0
        let infoLabelsRightInset: CGFloat = 32.0
        
        let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0
        
        let (titleString, descriptionString, hasArtist, caption) = stringsForDisplayData(self.displayData, presentationData: self.presentationData)
        
        if self.previousCaption?.string != caption?.string {
            self.previousCaption = caption
            let chapters = caption.flatMap { parseMediaPlayerChapters($0) } ?? []
            self.chaptersPromise.set(chapters)
            self.scrubberNode.updateContent(.standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .circle, backgroundColor: self.presentationData.theme.list.controlSecondaryColor, foregroundColor: self.presentationData.theme.list.itemAccentColor, bufferingColor: self.presentationData.theme.list.itemAccentColor.withAlphaComponent(0.4), chapters: chapters))
        }
        
        self.artistButton.isUserInteractionEnabled = hasArtist
        let makeTitleLayout = TextNode.asyncLayout(self.titleNode)
        let (titleLayout, titleApply) = makeTitleLayout(TextNodeLayoutArguments(attributedString: titleString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
        let makeDescriptionLayout = TextNode.asyncLayout(self.descriptionNode)
        let (descriptionLayout, descriptionApply) = makeDescriptionLayout(TextNodeLayoutArguments(attributedString: descriptionString, backgroundColor: nil, maximumNumberOfLines: 1, truncationType: .end, constrainedSize: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset - infoLabelsLeftInset - infoLabelsRightInset, height: CGFloat.greatestFiniteMagnitude), alignment: .left, lineSpacing: 0.0, cutout: nil, insets: UIEdgeInsets()))
        
        transition.updateFrame(node: self.titleNode, frame: CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - titleLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 1.0), size: titleLayout.size))
        let _ = titleApply()
        
        let descriptionFrame = CGRect(origin: CGPoint(x: self.isExpanded ? floor((width - descriptionLayout.size.width) / 2.0) : (leftInset + sideInset + infoLabelsLeftInset), y: infoVerticalOrigin + 24.0), size: descriptionLayout.size)
        transition.updateFrame(node: self.descriptionNode, frame: descriptionFrame)
        let _ = descriptionApply()
        
        self.artistButton.frame = descriptionFrame.insetBy(dx: -8.0, dy: -8.0)
        
        var albumArt: SharedMediaPlaybackAlbumArt?
        if let displayData = self.displayData {
            switch displayData {
                case let .music(_, _, value, _, _):
                    albumArt = value
                default:
                    break
            }
        }
        if self.currentAlbumArt != albumArt || !self.currentAlbumArtInitialized {
            self.currentAlbumArtInitialized = true
            self.currentAlbumArt = albumArt
            self.albumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: true))
            if let largeAlbumArtNode = self.largeAlbumArtNode {
                largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: albumArt, thumbnail: false))
            }
        }
    }
    
    private func updatePlayPauseButton(paused: Bool, animated: Bool) {
        self.playPauseIconNode.customColor = self.presentationData.theme.list.itemPrimaryTextColor
        if paused {
            self.playPauseIconNode.enqueueState(.play, animated: animated)
        } else {
            self.playPauseIconNode.enqueueState(.pause, animated: animated)
        }
    }
    
    private func updateOrderButton(_ order: MusicPlaybackSettingsOrder) {
        switch order {
            case .regular:
                self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: self.presentationData.theme.list.itemSecondaryTextColor)
            case .reversed:
                self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderReverse"), color: self.presentationData.theme.list.itemAccentColor)
            case .random:
                self.orderButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/OrderRandom"), color: self.presentationData.theme.list.itemAccentColor)
        }
    }
    
    private func updateLoopButton(_ looping: MusicPlaybackSettingsLooping) {
        let baseColor = self.presentationData.theme.list.itemSecondaryTextColor
        switch looping {
            case .none:
                self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: baseColor)
            case .item:
                self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/RepeatOne"), color: self.presentationData.theme.list.itemAccentColor)
            case .all:
                self.loopingButton.icon = generateTintedImage(image: UIImage(bundleImageName: "GlobalMusicPlayer/Repeat"), color: self.presentationData.theme.list.itemAccentColor)
        }
    }
    
    private func updateRateButton(_ playbackBaseRate: AudioPlaybackRate) {
        let rate = self.previousRate ?? playbackBaseRate
        
        self.rateButton.setContent(.image(optionsRateImage(rate: rate.stringValue.uppercased(), color: self.presentationData.theme.list.itemSecondaryTextColor)))
    }
    
    static let basePanelHeight: CGFloat = 220.0
    
    static func heightForLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, isExpanded: Bool) -> CGFloat {
        var panelHeight: CGFloat = OverlayPlayerControlsNode.basePanelHeight
        if isExpanded {
            let sideInset: CGFloat = 20.0
            panelHeight += width - leftInset - rightInset - sideInset * 2.0 + 24.0
        }
        return min(panelHeight, maxHeight)
    }
    
    func updateLayout(width: CGFloat, leftInset: CGFloat, rightInset: CGFloat, maxHeight: CGFloat, transition: ContainedViewLayoutTransition) -> CGFloat {
        self.validLayout = (width, leftInset, rightInset, maxHeight)
    
        let panelHeight = OverlayPlayerControlsNode.heightForLayout(width: width, leftInset: leftInset, rightInset: rightInset, maxHeight: maxHeight, isExpanded: self.isExpanded)
        
        transition.updateFrame(node: self.separatorNode, frame: CGRect(origin: CGPoint(x: 0.0, y: panelHeight), size: CGSize(width: width, height: UIScreenPixel)))
        
        transition.updateFrame(node: self.collapseNode, frame: CGRect(origin: CGPoint(x: 0.0, y: 2.0), size: CGSize(width: width, height: 30.0)))
        
        let sideInset: CGFloat = 20.0
        let sideButtonsInset: CGFloat = sideInset + 36.0
        
        let infoVerticalOrigin: CGFloat = panelHeight - OverlayPlayerControlsNode.basePanelHeight + 36.0
        
        self.updateLabels(transition: transition)
        
        transition.updateFrame(node: self.shareNode, frame: CGRect(origin: CGPoint(x: width - rightInset - sideInset - 32.0, y: infoVerticalOrigin + 2.0), size: CGSize(width: 42.0, height: 42.0)))
        
        let albumArtSize = CGSize(width: 48.0, height: 48.0)
        let makeAlbumArtLayout = self.albumArtNode.asyncLayout()
        let applyAlbumArt = makeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: albumArtSize, boundingSize: albumArtSize, intrinsicInsets: UIEdgeInsets()))
        applyAlbumArt()
        let albumArtFrame = CGRect(origin: CGPoint(x: leftInset + sideInset, y: infoVerticalOrigin - 1.0), size: albumArtSize)
        let previousAlbumArtNodeFrame = self.albumArtNode.frame
        transition.updateFrame(node: self.albumArtNode, frame: albumArtFrame)
        
        if self.isExpanded {
            let largeAlbumArtNode: TransformImageNode
            var animateIn = false
            if let current = self.largeAlbumArtNode {
                largeAlbumArtNode = current
            } else {
                animateIn = true
                largeAlbumArtNode = TransformImageNode()
                if self.isNodeLoaded {
                    largeAlbumArtNode.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.albumArtTap(_:))))
                }
                self.largeAlbumArtNode = largeAlbumArtNode
                self.addSubnode(largeAlbumArtNode)
                if self.currentAlbumArtInitialized {
                    largeAlbumArtNode.setSignal(playerAlbumArt(postbox: self.account.postbox, engine: self.engine, fileReference: self.currentFileReference, albumArt: self.currentAlbumArt, thumbnail: false))
                }
            }
            
            let albumArtHeight = max(1.0, panelHeight - OverlayPlayerControlsNode.basePanelHeight - 24.0)
            
            let largeAlbumArtSize = CGSize(width: albumArtHeight, height: albumArtHeight)
            let makeLargeAlbumArtLayout = largeAlbumArtNode.asyncLayout()
            let applyLargeAlbumArt = makeLargeAlbumArtLayout(TransformImageArguments(corners: ImageCorners(radius: 4.0), imageSize: largeAlbumArtSize, boundingSize: largeAlbumArtSize, intrinsicInsets: UIEdgeInsets()))
            applyLargeAlbumArt()
            
            let largeAlbumArtFrame = CGRect(origin: CGPoint(x: floor((width - largeAlbumArtSize.width) / 2.0), y: 34.0), size: largeAlbumArtSize)
            
            if animateIn && transition.isAnimated {
                largeAlbumArtNode.frame = largeAlbumArtFrame
                transition.animatePositionAdditive(node: largeAlbumArtNode, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y))
                //largeAlbumArtNode.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, additive: true)
                transition.animateTransformScale(node: largeAlbumArtNode, from: previousAlbumArtNodeFrame.size.height / largeAlbumArtFrame.size.height)
                largeAlbumArtNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
                if let copyView = self.albumArtNode.view.snapshotContentTree() {
                    copyView.frame = previousAlbumArtNodeFrame
                    copyView.center = largeAlbumArtFrame.center
                    self.view.insertSubview(copyView, belowSubview: largeAlbumArtNode.view)
                    transition.animatePositionAdditive(layer: copyView.layer, offset: CGPoint(x: previousAlbumArtNodeFrame.center.x - largeAlbumArtFrame.center.x, y: previousAlbumArtNodeFrame.center.y - largeAlbumArtFrame.center.y), completion: { [weak copyView] _ in
                        copyView?.removeFromSuperview()
                    })
                    //copyView.layer.animatePosition(from: CGPoint(x: -50.0, y: 0.0), to: CGPoint(), duration: 0.15, timingFunction: CAMediaTimingFunctionName.easeInEaseOut.rawValue, additive: true)
                    copyView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, removeOnCompletion: false)
                    transition.updateTransformScale(layer: copyView.layer, scale: largeAlbumArtFrame.size.height / previousAlbumArtNodeFrame.size.height)
                }
            } else {
                transition.updateFrame(node: largeAlbumArtNode, frame: largeAlbumArtFrame)
            }
            self.albumArtNode.isHidden = true
        } else if let largeAlbumArtNode = self.largeAlbumArtNode {
            self.largeAlbumArtNode = nil
            self.albumArtNode.isHidden = false
            if transition.isAnimated {
                transition.animatePosition(node: self.albumArtNode, from: largeAlbumArtNode.frame.center)
                transition.animateTransformScale(node: self.albumArtNode, from: largeAlbumArtNode.frame.height / self.albumArtNode.frame.height)
                self.albumArtNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.12)
                
                transition.updatePosition(node: largeAlbumArtNode, position: self.albumArtNode.frame.center, completion: { [weak largeAlbumArtNode] _ in
                    largeAlbumArtNode?.removeFromSupernode()
                })
                largeAlbumArtNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.28, removeOnCompletion: false)
                transition.updateTransformScale(node: largeAlbumArtNode, scale: self.albumArtNode.frame.height / largeAlbumArtNode.frame.height)
            } else {
                largeAlbumArtNode.removeFromSupernode()
            }
        }
        
        let scrubberVerticalOrigin: CGFloat = infoVerticalOrigin + 64.0
        
        transition.updateFrame(node: self.scrubberNode, frame: CGRect(origin: CGPoint(x: leftInset +  sideInset, y: scrubberVerticalOrigin - 8.0), size: CGSize(width: width - sideInset * 2.0 - leftInset - rightInset, height: 10.0 + 8.0 * 2.0)))
        
        let leftLabelVerticalOffset: CGFloat = self.leftDurationLabelPushed ? 6.0 : 0.0
        transition.updateFrame(node: self.leftDurationLabel, frame: CGRect(origin: CGPoint(x: leftInset + sideInset, y: scrubberVerticalOrigin + 14.0 + leftLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0)))
        
        let rightLabelVerticalOffset: CGFloat = self.rightDurationLabelPushed ? 6.0 : 0.0
        transition.updateFrame(node: self.rightDurationLabel, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - 100.0, y: scrubberVerticalOrigin + 14.0 + rightLabelVerticalOffset), size: CGSize(width: 100.0, height: 20.0)))
        
        let infoLabelVerticalOffset: CGFloat = self.infoNodePushed ? 6.0 : 0.0
                
        let infoSize = self.infoNode.measure(CGSize(width: width - 60.0 * 2.0 - 100.0, height: 100.0))
        self.infoNode.bounds = CGRect(origin: CGPoint(), size: infoSize)
        transition.updatePosition(node: self.infoNode, position: CGPoint(x: width / 2.0, y: scrubberVerticalOrigin + 14.0 + infoLabelVerticalOffset + infoSize.height / 2.0))
        
        
        let rateRightOffset = timestampLabelWidthForDuration(self.currentDuration)
        transition.updateFrame(node: self.rateButton, frame: CGRect(origin: CGPoint(x: width - sideInset - rightInset - rateRightOffset - 28.0, y: scrubberVerticalOrigin + 10.0 + rightLabelVerticalOffset - 10.0), size: CGSize(width: 24.0, height: 44.0)))
        
        transition.updateFrame(node: self.backgroundNode, frame: CGRect(origin: CGPoint(x: 0.0, y: -8.0), size: CGSize(width: width, height: panelHeight + 8.0)))
        
        let buttonSize = CGSize(width: 64.0, height: 64.0)
        let buttonsWidth = min(width - leftInset - rightInset - sideButtonsInset * 2.0, 320.0)
        let buttonsRect = CGRect(origin: CGPoint(x: floor((width - buttonsWidth) / 2.0), y: scrubberVerticalOrigin + 36.0), size: CGSize(width: buttonsWidth, height: buttonSize.height))
        
        transition.updateFrame(node: self.orderButton, frame: CGRect(origin: CGPoint(x: leftInset + sideInset - 22.0, y: buttonsRect.minY), size: buttonSize))
        transition.updateFrame(node: self.loopingButton, frame: CGRect(origin: CGPoint(x: width - rightInset - sideInset - buttonSize.width + 22.0, y: buttonsRect.minY), size: buttonSize))
        
        transition.updateFrame(node: self.backwardButton, frame: CGRect(origin: buttonsRect.origin, size: buttonSize))
        transition.updateFrame(node: self.forwardButton, frame: CGRect(origin: CGPoint(x: buttonsRect.maxX - buttonSize.width, y: buttonsRect.minY), size: buttonSize))
        
        let playPauseFrame = CGRect(origin: CGPoint(x: buttonsRect.minX + floor((buttonsRect.width - buttonSize.width) / 2.0), y: buttonsRect.minY), size: buttonSize)
        transition.updateFrame(node: self.playPauseButton, frame: playPauseFrame)
        transition.updateFrame(node: self.playPauseIconNode, frame: CGRect(origin: CGPoint(x: -6.0, y: -6.0), size: CGSize(width: 76.0, height: 76.0)))
        
        return panelHeight
    }
    
    func collapse() {
        if self.isExpanded {
            self.isExpanded = false
            self.updateIsExpanded?()
        }
    }
    
    @objc func collapsePressed() {
        self.requestCollapse?()
    }
    
    @objc func sharePressed() {
        if let itemId = self.currentItemId as? PeerMessagesMediaPlaylistItemId {
            self.requestShare?(itemId.messageId)
        }
    }
    
    @objc func orderPressed() {
        if let order = self.currentOrder {
            let nextOrder: MusicPlaybackSettingsOrder
            switch order {
                case .regular:
                    nextOrder = .reversed
                case .reversed:
                    nextOrder = .random
                case .random:
                    nextOrder = .regular
            }
            let _ = updateMusicPlaybackSettingsInteractively(accountManager: self.accountManager, {
                return $0.withUpdatedOrder(nextOrder)
            }).startStandalone()
            self.control?(.setOrder(nextOrder))
        }
    }
    
    @objc func loopingPressed() {
        if let looping = self.currentLooping {
            let nextLooping: MusicPlaybackSettingsLooping
            switch looping {
                case .none:
                    nextLooping = .item
                case .item:
                    nextLooping = .all
                case .all:
                    nextLooping = .none
            }
            let _ = updateMusicPlaybackSettingsInteractively(accountManager: self.accountManager, {
                return $0.withUpdatedLooping(nextLooping)
            }).startStandalone()
            self.control?(.setLooping(nextLooping))
        }
    }
    
    @objc func backwardPressed() {
        self.control?(.previous)
    }
    
    @objc func forwardPressed() {
        self.control?(.next)
    }
    
    @objc func playPausePressed() {
        self.control?(.playback(.togglePlayPause))
    }
    
    @objc func rateButtonPressed() {
        var nextRate: AudioPlaybackRate
        if let rate = self.currentRate {
            switch rate {
            case .x0_5, .x2:
                nextRate = .x1
            case .x1:
                nextRate = .x1_5
            case .x1_5:
                nextRate = .x2
            default:
                if rate.doubleValue < 0.5 {
                    nextRate = .x0_5
                } else if rate.doubleValue < 1.0 {
                    nextRate = .x1
                } else if rate.doubleValue < 1.5 {
                    nextRate = .x1_5
                } else if rate.doubleValue < 2.0 {
                    nextRate = .x2
                } else {
                    nextRate = .x1
                }
            }
        } else {
            nextRate = .x1_5
        }
        self.control?(.setBaseRate(nextRate))
    }
    
    private func speedList(strings: PresentationStrings) -> [(String, String, AudioPlaybackRate)] {
        let speedList: [(String, String, AudioPlaybackRate)] = [
            ("0.5x", "0.5x", .x0_5),
            (strings.PlaybackSpeed_Normal, "1x", .x1),
            ("1.5x", "1.5x", .x1_5),
            ("2x", "2x", .x2)
        ]
        return speedList
    }
    
    private func contextMenuSpeedItems(scheduleTooltip: @escaping (MediaNavigationAccessoryPanel.ChangeType?) -> Void) -> Signal<ContextController.Items, NoError> {
        var presetItems: [ContextMenuItem] = []
                
        let previousRate = self.currentRate
        let previousValue = self.currentRate?.doubleValue ?? 1.0
        let sliderValuePromise = ValuePromise<Double?>(nil)
        let sliderItem: ContextMenuItem = .custom(SliderContextItem(minValue: 0.2, maxValue: 2.5, value: previousValue, valueChanged: { [weak self] newValue, finished in
            let newValue = normalizeValue(newValue)
            self?.control?(.setBaseRate(AudioPlaybackRate(newValue)))
            sliderValuePromise.set(newValue)
            if finished {
                scheduleTooltip(.sliderCommit(previousValue, newValue))
            }
        }), true)
 
        let theme = self.presentationData.theme
        for (text, _, rate) in self.speedList(strings: self.presentationData.strings) {
            let isSelected = self.currentRate == rate
            presetItems.append(.action(ContextMenuActionItem(text: text, icon: { _ in return nil }, iconSource: ContextMenuActionItemIconSource(size: CGSize(width: 24.0, height: 24.0), signal: sliderValuePromise.get()
            |> map { value in
                if isSelected && value == nil {
                    return generateTintedImage(image: UIImage(bundleImageName: "Chat/Context Menu/Check"), color: theme.contextMenu.primaryColor)
                } else {
                    return nil
                }
            }), action: { [weak self] _, f in
                scheduleTooltip(nil)
                f(.default)
                
                self?.control?(.setBaseRate(rate))
                if let previousRate, previousRate.isPreset {
                    self?.presentAudioRateTooltip(baseRate: rate, changeType: .preset)
                } else {
                    self?.presentAudioRateTooltip(baseRate: rate, changeType: .sliderCommit(previousValue, rate.doubleValue))
                }
            })))
        }

        return .single(ContextController.Items(content: .twoLists(presetItems, [sliderItem])))
    }
    
    private func openRateMenu(sourceNode: ASDisplayNode, gesture: ContextGesture?) {
        guard let controller = self.getParentController() else {
            return
        }
        var scheduledTooltip: MediaNavigationAccessoryPanel.ChangeType?
        let items = self.contextMenuSpeedItems(scheduleTooltip: { change in
            scheduledTooltip = change
        })
        
        let contextController = ContextController(presentationData: self.presentationData, source: .reference(HeaderContextReferenceContentSource(controller: controller, sourceNode: self.rateButton.referenceNode, shouldBeDismissed: .single(false))), items: items, gesture: gesture)
        contextController.dismissed = { [weak self] in
            if let scheduledTooltip, let self, let rate = self.currentRate {
                self.presentAudioRateTooltip(baseRate: rate, changeType: scheduledTooltip)
            }
        }
        controller.presentInGlobalOverlay(contextController)
    }
    
    private func presentAudioRateTooltip(baseRate: AudioPlaybackRate, changeType: MediaNavigationAccessoryPanel.ChangeType) {
        guard let controller = self.getParentController() else {
            return
        }
        
        let presentationData = self.presentationData
        let text: String?
        let rate: CGFloat?
        if case let .sliderCommit(previousValue, newValue) = changeType {
            let value = String(format: "%0.1f", baseRate.doubleValue)
            if baseRate == .x1 {
                text = presentationData.strings.Conversation_AudioRateTooltipNormal
            } else {
                text = presentationData.strings.Conversation_AudioRateTooltipCustom(value).string
            }
            if newValue > previousValue {
                rate = .infinity
            } else if newValue < previousValue {
                rate = -.infinity
            } else {
                rate = nil
            }
        } else if baseRate == .x1 {
            text = presentationData.strings.Conversation_AudioRateTooltipNormal
            rate = 1.0
        } else if baseRate == .x1_5 {
            text = presentationData.strings.Conversation_AudioRateTooltip15X
            rate = 1.5
        } else if baseRate == .x2 {
            text = presentationData.strings.Conversation_AudioRateTooltipSpeedUp
            rate = 2.0
        } else {
            text = nil
            rate = nil
        }
        var showTooltip = true
        if case .sliderChange = changeType {
            showTooltip = false
        }
        if let rate, let text, showTooltip {
            controller.presentInGlobalOverlay(
                UndoOverlayController(
                    presentationData: presentationData,
                    content: .audioRate(
                        rate: rate,
                        text: text
                    ),
                    elevatedLayout: false,
                    animateInAsReplacement: false,
                    action: { action in
                        return true
                    }
                )
            )
        }
    }
    
    @objc func albumArtTap(_ recognizer: UITapGestureRecognizer) {
        if case .ended = recognizer.state {
            if let supernode = self.supernode {
                let bounds = supernode.bounds
                if bounds.width > bounds.height {
                    return
                }
            }
            self.isExpanded = !self.isExpanded
            self.updateIsExpanded?()
        }
    }
    
    @objc func artistPressed() {
        let (_, descriptionString, _, _) = stringsForDisplayData(self.displayData, presentationData: self.presentationData)
        if let artist = descriptionString?.string {
            self.requestSearchByArtist?(artist)
        }
    }
    
    override func hitTest(_ point: CGPoint, with event: UIEvent?) -> UIView? {
        let result = super.hitTest(point, with: event)
        if result == self.view {
            return nil
        }
        return result
    }
}

private enum PlayPauseIconNodeState: Equatable {
    case play
    case pause
}

private final class PlayPauseIconNode: ManagedAnimationNode {
    private let duration: Double = 0.35
    private var iconState: PlayPauseIconNodeState = .pause
    
    init() {
        super.init(size: CGSize(width: 76.0, height: 76.0))
        
        self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
    }
    
    func enqueueState(_ state: PlayPauseIconNodeState, animated: Bool) {
        guard self.iconState != state else {
            return
        }
        
        let previousState = self.iconState
        self.iconState = state
        
        switch previousState {
            case .pause:
                switch state {
                    case .play:
                        if animated {
                            self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 83), duration: self.duration))
                        } else {
                            self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 0), duration: 0.01))
                        }
                    case .pause:
                        break
                }
            case .play:
                switch state {
                    case .pause:
                        if animated {
                            self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 0, endFrame: 41), duration: self.duration))
                        } else {
                            self.trackTo(item: ManagedAnimationItem(source: .local("anim_playpause"), frames: .range(startFrame: 41, endFrame: 41), duration: 0.01))
                        }
                    case .play:
                        break
                }
        }
    }
}

private final class HeaderContextReferenceContentSource: ContextReferenceContentSource {
    private let controller: ViewController
    private let sourceNode: ContextReferenceContentNode

    var shouldBeDismissed: Signal<Bool, NoError>
    
    init(controller: ViewController, sourceNode: ContextReferenceContentNode, shouldBeDismissed: Signal<Bool, NoError>) {
        self.controller = controller
        self.sourceNode = sourceNode
        self.shouldBeDismissed = shouldBeDismissed
    }
    
    func transitionInfo() -> ContextControllerReferenceViewInfo? {
        return ContextControllerReferenceViewInfo(referenceView: self.sourceNode.view, contentAreaInScreenSpace: UIScreen.main.bounds)
    }
}