import Foundation
import UIKit
import AsyncDisplayKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramStringFormatting
import LegacyComponents
import CheckNode
import MosaicLayout
import WallpaperBackgroundNode
import AccountContext
import ChatMessageBackground

private class MediaPickerSelectedItemNode: ASDisplayNode {
    let asset: TGMediaEditableItem
    private let interaction: MediaPickerInteraction?
    private let enableAnimations: Bool
    
    private let imageNode: ImageNode
    private var checkNode: InteractiveCheckNode?
    private var durationBackgroundNode: ASDisplayNode?
    private var durationTextNode: ImmediateTextNode?
    
    private var adjustmentsDisposable: Disposable?
    
    private let spoilerDisposable = MetaDisposable()
    private var spoilerNode: SpoilerOverlayNode?
    
    private var theme: PresentationTheme?
    
    private var validLayout: CGSize?
    
    var corners: CACornerMask = [] {
        didSet {
            if #available(iOS 13.0, *) {
                self.layer.cornerCurve = .circular
            }
            if #available(iOS 11.0, *) {
                self.layer.maskedCorners = corners
            }
        }
    }
    
    var radius: CGFloat = 0.0 {
        didSet {
            self.layer.cornerRadius = radius
        }
    }
    
    private var readyPromise = Promise<Bool>()
    fileprivate var ready: Signal<Bool, NoError> {
        return self.readyPromise.get()
    }
    
    private var videoDuration: Double?
    
    init(asset: TGMediaEditableItem, interaction: MediaPickerInteraction?, enableAnimations: Bool) {
        self.asset = asset
        self.interaction = interaction
        self.enableAnimations = enableAnimations
        
        self.imageNode = ImageNode()
        self.imageNode.contentMode = .scaleAspectFill
        self.imageNode.clipsToBounds = true
        self.imageNode.animateFirstTransition = false
        
        super.init()
        
        self.clipsToBounds = true
        
        self.addSubnode(self.imageNode)
        
        if let editingState = interaction?.editingState {
            if asset.isVideo {
                func adjustmentsChangedSignal(editingState: TGMediaEditingContext) -> Signal<TGMediaEditAdjustments?, NoError> {
                    return Signal { subscriber in
                        let disposable = editingState.adjustmentsSignal(for: asset).start(next: { next in
                            if let next = next as? TGMediaEditAdjustments {
                                subscriber.putNext(next)
                            } else if next == nil {
                                subscriber.putNext(nil)
                            }
                        }, error: nil, completed: {})
                        return ActionDisposable {
                            disposable?.dispose()
                        }
                    }
                }
                
                self.adjustmentsDisposable = (adjustmentsChangedSignal(editingState: editingState)
                                              |> deliverOnMainQueue).start(next: { [weak self] adjustments in
                    if let strongSelf = self {
                        let duration: Double
                        if let adjustments = adjustments as? TGVideoEditAdjustments, adjustments.trimApplied() {
                            duration = adjustments.trimEndValue - adjustments.trimStartValue
                        } else {
                            duration = asset.originalDuration ?? 0.0
                        }
                        strongSelf.videoDuration = duration
                        
                        if let size = strongSelf.validLayout {
                            strongSelf.updateLayout(size: size, transition: .immediate)
                        }
                    }
                })
            }
            
            let spoilerSignal = Signal<Bool, NoError> { subscriber in
                if let signal = editingState.spoilerSignal(forIdentifier: asset.uniqueIdentifier) {
                    let disposable = signal.start(next: { next in
                        if let next = next as? Bool {
                            subscriber.putNext(next)
                        }
                    }, error: { _ in
                    }, completed: nil)!
                    
                    return ActionDisposable {
                        disposable.dispose()
                    }
                } else {
                    return EmptyDisposable
                }
            }
            
            self.spoilerDisposable.set((spoilerSignal
            |> deliverOnMainQueue).start(next: { [weak self] hasSpoiler in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.updateHasSpoiler(hasSpoiler)
            }))
        }
        
        self.imageNode.contentUpdated = { [weak self] image in
            self?.spoilerNode?.setImage(image)
        }
    }
    
    deinit {
        self.adjustmentsDisposable?.dispose()
        self.spoilerDisposable.dispose()
    }
    
    override func didLoad() {
        super.didLoad()
        
        self.view.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.tap)))
    }
    
    @objc private func tap() {
        guard let asset = self.asset as? TGMediaSelectableItem else {
            return
        }
        self.interaction?.openSelectedMedia(asset, self.imageNode.image)
    }
    
    private var didSetupSpoiler = false
    private func updateHasSpoiler(_ hasSpoiler: Bool) {
        var animated = true
        if !self.didSetupSpoiler {
            animated = false
            self.didSetupSpoiler = true
        }
    
        if hasSpoiler {
            if self.spoilerNode == nil {
                let spoilerNode = SpoilerOverlayNode(enableAnimations: self.enableAnimations)
                self.insertSubnode(spoilerNode, aboveSubnode: self.imageNode)
                self.spoilerNode = spoilerNode
                
                spoilerNode.setImage(self.imageNode.image)
                
                if animated {
                    spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                }
            }
            self.spoilerNode?.update(size: self.bounds.size, transition: .immediate)
            self.spoilerNode?.frame = CGRect(origin: .zero, size: self.bounds.size)
        } else if let spoilerNode = self.spoilerNode {
            self.spoilerNode = nil
            spoilerNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak spoilerNode] _ in
                spoilerNode?.removeFromSupernode()
            })
        }
    }
    
    func setup(size: CGSize) {
        let editingState = self.interaction?.editingState
        let editedSignal = Signal<UIImage?, NoError> { subscriber in
            if let editingState = editingState, let signal = editingState.thumbnailImageSignal(forIdentifier: self.asset.uniqueIdentifier) {
                let disposable = signal.start(next: { next in
                    if let image = next as? UIImage {
                        subscriber.putNext(image)
                    } else {
                        subscriber.putNext(nil)
                    }
                }, error: { _ in
                }, completed: nil)!
                
                return ActionDisposable {
                    disposable.dispose()
                }
            } else {
                return EmptyDisposable
            }
        }
        
        let dimensions: CGSize
        if let adjustments = self.interaction?.editingState.adjustments(for: self.asset), adjustments.cropApplied(forAvatar: false) {
            dimensions = adjustments.cropRect.size
        } else {
            dimensions = self.asset.originalSize ?? CGSize()
        }
        
        let scale = min(2.0, UIScreenScale)
        let scaledDimensions = dimensions.aspectFilled(CGSize(width: 320.0, height: 320.0))
        let targetSize = CGSize(width: scaledDimensions.width * scale, height: scaledDimensions.height * scale)
        
        let originalSignal: Signal<UIImage?, NoError>
        if let asset = self.asset as? TGMediaAsset {
            originalSignal = assetImage(asset: asset.backingAsset, targetSize: targetSize, exact: false)
        } else {
            let asset = self.asset
            originalSignal = Signal<UIImage?, NoError> { subscriber in
                let disposable = asset.screenImageSignal?(0.0).start(next: { next in
                    if let next = next as? UIImage {
                        subscriber.putNext(next)
                    }
                }, error: { _ in
                }, completed: {
                    subscriber.putCompletion()
                })
                
                return ActionDisposable {
                    disposable?.dispose()
                }
            }
        }
        let imageSignal: Signal<UIImage?, NoError> = editedSignal
        |> mapToSignal { result in
            if let result = result {
                return .single(result)
            } else {
                return originalSignal
            }
        }
        self.imageNode.setSignal(imageSignal)
        self.readyPromise.set(self.imageNode.contentReady)
    }
    
    func updateSelectionState() {
        if self.checkNode == nil, let _ = self.interaction?.selectionState, let theme = self.theme {
            let checkNode = InteractiveCheckNode(theme: CheckNodeTheme(theme: theme, style: .overlay))
            checkNode.valueChanged = { [weak self] value in
                if let strongSelf = self, let interaction = strongSelf.interaction, let selectableItem = strongSelf.asset as? TGMediaSelectableItem {
                    if !interaction.toggleSelection(selectableItem, value, true) {
                        strongSelf.checkNode?.setSelected(false, animated: false)
                    }
                }
            }
            self.addSubnode(checkNode)
            self.checkNode = checkNode

            if let size = self.validLayout {
                self.updateLayout(size: size, transition: .immediate)
            }
        }
        
        if let interaction = self.interaction, let selectionState = interaction.selectionState, let selectableItem = self.asset as? TGMediaSelectableItem {
            let selected = selectionState.isIdentifierSelected(selectableItem.uniqueIdentifier)
            let index = selectionState.index(of: selectableItem)
            if index != NSNotFound {
                self.checkNode?.content = .counter(Int(index))
            }
            self.checkNode?.setSelected(selected, animated: false)
            
            if let checkNode = self.checkNode {
                let transition = ContainedViewLayoutTransition.animated(duration: 0.2, curve: .easeInOut)
                transition.updateAlpha(node: checkNode, alpha: selectionState.count() < 2 ? 0.0 : 1.0)
            }
        }
    }
    
    func updateHiddenMedia() {
        let wasHidden = self.isHidden
        self.isHidden = self.interaction?.hiddenMediaId == asset.uniqueIdentifier
        if !self.isHidden && wasHidden {
            if let checkNode = self.checkNode, checkNode.alpha > 0.0 {
                checkNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
            }
            
            if let durationTextNode = self.durationTextNode, durationTextNode.alpha > 0.0 {
                durationTextNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
            }
            if let durationBackgroundNode = self.durationBackgroundNode, durationBackgroundNode.alpha > 0.0 {
                durationBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
            }
            
            if let spoilerNode = self.spoilerNode, spoilerNode.alpha > 0.0 {
                spoilerNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.3)
            }
        }
    }
    
    func update(theme: PresentationTheme) {
        var updatedTheme = false
        if self.theme != theme {
            self.theme = theme
            updatedTheme = true
        }
        
        if updatedTheme {
            self.checkNode?.theme = CheckNodeTheme(theme: theme, style: .overlay)
        }
    }
    
    func updateLayout(size: CGSize, transition: ContainedViewLayoutTransition) {
        self.validLayout = size
        
        transition.updateFrame(node: self.imageNode, frame: CGRect(origin: CGPoint(), size: size))
        
        if let spoilerNode = self.spoilerNode {
            transition.updateFrame(node: spoilerNode, frame: CGRect(origin: CGPoint(), size: size))
            spoilerNode.update(size: size, transition: transition)
        }
        
        let checkSize = CGSize(width: 29.0, height: 29.0)
        if let checkNode = self.checkNode {
            transition.updateFrame(node: checkNode, frame: CGRect(origin: CGPoint(x: size.width - checkSize.width - 3.0, y: 3.0), size: checkSize))
        }
        
        if let duration = self.videoDuration {
            let textNode: ImmediateTextNode
            let backgroundNode: ASDisplayNode
            if let currentTextNode = self.durationTextNode, let currentBackgroundNode = self.durationBackgroundNode {
                textNode = currentTextNode
                backgroundNode = currentBackgroundNode
            } else {
                backgroundNode = ASDisplayNode()
                backgroundNode.backgroundColor = UIColor(rgb: 0x000000, alpha: 0.5)
                backgroundNode.cornerRadius = 9.0
                self.addSubnode(backgroundNode)
                self.durationBackgroundNode = backgroundNode
                
                textNode = ImmediateTextNode()
                textNode.displaysAsynchronously = false
                self.addSubnode(textNode)
                self.durationTextNode = textNode
            }
            
            textNode.attributedText = NSAttributedString(string: stringForDuration(Int32(duration)), font: Font.with(size: 11.0, design: .regular, weight: .regular, traits: .monospacedNumbers), textColor: .white)
            
            let textSize = textNode.updateLayout(size)
            let backgroundFrame = CGRect(x: 6.0, y: 6.0, width: ceil(textSize.width) + 14.0, height: 18.0)
            backgroundNode.frame = backgroundFrame
            textNode.frame = CGRect(origin: CGPoint(x: backgroundFrame.minX + floorToScreenPixels((backgroundFrame.size.width - textSize.width) / 2.0), y: backgroundFrame.minY + floorToScreenPixels((backgroundFrame.size.height - textSize.height) / 2.0)), size: textSize)
        } else {
            if let durationTextNode = self.durationTextNode {
                self.durationTextNode = nil
                durationTextNode.removeFromSupernode()
            }
            if let durationBackgroundNode = self.durationBackgroundNode {
                self.durationBackgroundNode = nil
                durationBackgroundNode.removeFromSupernode()
            }
        }
    }
    
    func transitionView() -> UIView {
        let view = self.imageNode.view.snapshotContentTree(unhide: true, keepTransform: true)!
        if #available(iOS 13.0, *) {
            view.layer.cornerCurve = self.layer.cornerCurve
        }
        if #available(iOS 11.0, *) {
            view.layer.maskedCorners = self.layer.maskedCorners
            view.layer.cornerRadius = self.layer.cornerRadius
        }
        view.frame = self.convert(self.bounds, to: nil)
        return view
    }
    
    func animateFrom(_ view: UIView) {
        view.alpha = 0.0
        
        let frame = view.convert(view.bounds, to: self.supernode?.view)
        let targetFrame = self.frame
        
        self.durationTextNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
        self.durationBackgroundNode?.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
        
        self.updateLayout(size: frame.size, transition: .immediate)
        self.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.25, curve: .spring))
        self.layer.animateFrame(from: frame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, completion: { [weak view] _ in
            view?.alpha = 1.0
        })
    }
    
    func animateTo(_ view: UIView, dustNode: ASDisplayNode?, completion: @escaping (Bool) -> Void) {
        view.alpha = 0.0
        
        let frame = self.frame
        let targetFrame = view.convert(view.bounds, to: self.supernode?.view)
        
        let corners = self.corners
        
        self.durationTextNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
        self.durationBackgroundNode?.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false)
        
        var dustSupernode: ASDisplayNode?
        var dustPosition: CGPoint?
        if let dustNode = dustNode {
            dustSupernode = dustNode.supernode
            dustPosition = dustNode.position
            
            self.addSubnode(dustNode)
            dustNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
            
            dustNode.layer.animatePosition(from: CGPoint(x: frame.width / 2.0, y: frame.height / 2.0), to: dustNode.position, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring)
            
            self.spoilerNode?.dustNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false)
        }
        
        self.corners = []
        self.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.25, curve: .spring))
        self.layer.animateFrame(from: frame, to: targetFrame, duration: 0.25, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, completion: { [weak view, weak self] _ in
            view?.alpha = 1.0
            
            self?.durationTextNode?.layer.removeAllAnimations()
            self?.durationBackgroundNode?.layer.removeAllAnimations()
            
            if let dustNode = dustNode {
                dustSupernode?.addSubnode(dustNode)
                dustNode.position = dustPosition ?? dustNode.position
            }
            
            var animateCheckNode = false
            if let strongSelf = self, let checkNode = strongSelf.checkNode, checkNode.alpha.isZero {
                animateCheckNode = true
            }
            completion(animateCheckNode)
                
            self?.corners = corners
            self?.updateLayout(size: frame.size, transition: .immediate)
            
            Queue.mainQueue().after(0.01) {
                self?.layer.removeAllAnimations()
                self?.spoilerNode?.dustNode.layer.removeAllAnimations()
            }
        })
    }
}

private class MessageBackgroundNode: ASDisplayNode {
    private let backgroundWallpaperNode: ChatMessageBubbleBackdrop
    private let backgroundNode: ChatMessageBackground
    private let shadowNode: ChatMessageShadowNode
    
    override init() {
        self.backgroundWallpaperNode = ChatMessageBubbleBackdrop()
        self.backgroundNode = ChatMessageBackground()
        self.shadowNode = ChatMessageShadowNode()

        super.init()
        
        self.addSubnode(self.backgroundWallpaperNode)
        self.addSubnode(self.backgroundNode)
    }
    
    private var absoluteRect: (CGRect, CGSize)?
    
    func update(size: CGSize, theme: PresentationTheme, wallpaper: TelegramWallpaper, graphics: PrincipalThemeEssentialGraphics, wallpaperBackgroundNode: WallpaperBackgroundNode, transition: ContainedViewLayoutTransition) {
        
        self.backgroundNode.setType(type: .outgoing(.Extracted), highlighted: false, graphics: graphics, maskMode: false, hasWallpaper: wallpaper.hasWallpaper, transition: transition, backgroundNode: wallpaperBackgroundNode)
        self.backgroundWallpaperNode.setType(type: .outgoing(.Extracted), theme: ChatPresentationThemeData(theme: theme, wallpaper: wallpaper), essentialGraphics: graphics, maskMode: true, backgroundNode: wallpaperBackgroundNode)
        self.shadowNode.setType(type: .outgoing(.Extracted), hasWallpaper: wallpaper.hasWallpaper, graphics: graphics)
        
        let backgroundFrame = CGRect(origin: CGPoint(), size: size)
        self.backgroundNode.updateLayout(size: backgroundFrame.size, transition: transition)
        self.backgroundWallpaperNode.updateFrame(backgroundFrame, transition: transition)
        self.shadowNode.updateLayout(backgroundFrame: backgroundFrame, transition: transition)
        
        if let (rect, size) = self.absoluteRect {
            self.updateAbsoluteRect(rect, within: size)
        }
    }
    
    func updateAbsoluteRect(_ rect: CGRect, within containerSize: CGSize) {
        self.absoluteRect = (rect, containerSize)
        
        var backgroundWallpaperFrame = self.backgroundWallpaperNode.frame
        backgroundWallpaperFrame.origin.x += rect.minX
        backgroundWallpaperFrame.origin.y += rect.minY
        self.backgroundWallpaperNode.update(rect: backgroundWallpaperFrame, within: containerSize)
    }
}

final class MediaPickerSelectedListNode: ASDisplayNode, UIScrollViewDelegate, UIGestureRecognizerDelegate {
    private let context: AccountContext
    private let persistentItems: Bool
    
    fileprivate let wallpaperBackgroundNode: WallpaperBackgroundNode
    private let scrollNode: ASScrollNode
    private var backgroundNodes: [Int: MessageBackgroundNode] = [:]
    private var itemNodes: [String: MediaPickerSelectedItemNode] = [:]
    
    private var reorderFeedback: HapticFeedback?
    private var reorderNode: ReorderingItemNode?
    private var isReordering = false

    private var graphics: PrincipalThemeEssentialGraphics?
    
    var interaction: MediaPickerInteraction?
    
    private var validLayout: (size: CGSize, insets: UIEdgeInsets, items: [TGMediaSelectableItem], grouped: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners)?
    
    private var didSetReady = false
    private var ready = Promise<Bool>()
    
    init(context: AccountContext, persistentItems: Bool) {
        self.context = context
        self.persistentItems = persistentItems
        
        self.wallpaperBackgroundNode = createWallpaperBackgroundNode(context: context, forChatDisplay: true, useSharedAnimationPhase: false)
        self.wallpaperBackgroundNode.backgroundColor = .black
        self.scrollNode = ASScrollNode()
        
        super.init()
        
        self.addSubnode(self.wallpaperBackgroundNode)
        self.addSubnode(self.scrollNode)
    }
    
    override func didLoad() {
        super.didLoad()
        
        if #available(iOS 11.0, *) {
            self.scrollNode.view.contentInsetAdjustmentBehavior = .never
        }
        
        self.scrollNode.view.delegate = self
        self.scrollNode.view.panGestureRecognizer.cancelsTouchesInView = true
        self.scrollNode.view.showsVerticalScrollIndicator = false
        
        self.view.addGestureRecognizer(ReorderingGestureRecognizer(animateOnTouch: !self.persistentItems, shouldBegin: { [weak self] point in
            if let strongSelf = self, !strongSelf.scrollNode.view.isDragging && strongSelf.itemNodes.count > 1 {
                let point = strongSelf.view.convert(point, to: strongSelf.scrollNode.view)
                for (_, itemNode) in strongSelf.itemNodes {
                    if itemNode.frame.contains(point) {
                        return (true, true, itemNode)
                    }
                }
                return (false, false, nil)
            }
            return (false, false, nil)
        }, willBegin: { _ in

        }, began: { [weak self] itemNode in
            self?.beginReordering(itemNode: itemNode)
        }, ended: { [weak self] point in
            if let strongSelf = self {
                if var point = point {
                    point = strongSelf.view.convert(point, to: strongSelf.scrollNode.view)
                    strongSelf.endReordering(point: point)
                } else {
                    strongSelf.endReordering(point: nil)
                }
            }
        }, moved: { [weak self] offset in
            self?.updateReordering(offset: offset)
        }))
        
        Queue.mainQueue().after(0.1, {
            self.updateAbsoluteRects()
        })
    }
    
    var getTransitionView: (String) -> (UIView, ASDisplayNode?, (Bool) -> Void)? = { _ in return nil }
    
    func animateIn(initiated: @escaping () -> Void, completion: @escaping () -> Void = {}) {
        let _ = (self.ready.get()
        |> take(1)
        |> deliverOnMainQueue).start(next: { [weak self] _ in
            guard let strongSelf = self else {
                return
            }
            
            strongSelf.alpha = 1.0
            initiated()
            
            strongSelf.wallpaperBackgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, completion: { _ in
                completion()
            })
            strongSelf.wallpaperBackgroundNode.layer.animateScale(from: 1.2, to: 1.0, duration: 0.33, timingFunction: kCAMediaTimingFunctionSpring)
            
            for (_, backgroundNode) in strongSelf.backgroundNodes {
                backgroundNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1)
            }
            
            for (identifier, itemNode) in strongSelf.itemNodes {
                if let (transitionView, _, _) = strongSelf.getTransitionView(identifier) {
                    itemNode.animateFrom(transitionView)
                } else {
                    itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.1)
                }
            }
            
            if let topNode = strongSelf.messageNodes?.first, !topNode.alpha.isZero {
                topNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1)
                topNode.layer.animatePosition(from: CGPoint(x: 0.0, y: -30.0), to: CGPoint(), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
            }
            
            if let bottomNode = strongSelf.messageNodes?.last, !bottomNode.alpha.isZero {
                bottomNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25, delay: 0.1)
                bottomNode.layer.animatePosition(from: CGPoint(x: 0.0, y: 30.0), to: CGPoint(), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, additive: true)
            }
        })
    }
    
    func animateOut(completion: @escaping () -> Void = {}) {
        self.wallpaperBackgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.25, removeOnCompletion: false, completion: { [weak self] _ in
            completion()
            
            if let strongSelf = self {
                Queue.mainQueue().after(0.01) {
                    for (_, backgroundNode) in strongSelf.backgroundNodes {
                        backgroundNode.layer.removeAllAnimations()
                    }
                    
                    for (_, itemNode) in strongSelf.itemNodes {
                        itemNode.layer.removeAllAnimations()
                    }
                    
                    strongSelf.messageNodes?.first?.layer.removeAllAnimations()
                    strongSelf.messageNodes?.last?.layer.removeAllAnimations()
                    
                    strongSelf.wallpaperBackgroundNode.layer.removeAllAnimations()
                }
            }
        })
        
        self.wallpaperBackgroundNode.layer.animateScale(from: 1.0, to: 1.2, duration: 0.33, timingFunction: kCAMediaTimingFunctionSpring)
        
        for (_, backgroundNode) in self.backgroundNodes {
            backgroundNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false)
        }
        
        for (identifier, itemNode) in self.itemNodes {
            if let (transitionView, maybeDustNode, completion) = self.getTransitionView(identifier) {
                itemNode.animateTo(transitionView, dustNode: maybeDustNode, completion: completion)
            } else {
                itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.1, removeOnCompletion: false)
            }
        }
        
        if let topNode = self.messageNodes?.first {
            topNode.layer.animateAlpha(from: topNode.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false)
            topNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: -30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
        }
        
        if let bottomNode = self.messageNodes?.last {
            bottomNode.layer.animateAlpha(from: bottomNode.alpha, to: 0.0, duration: 0.15, removeOnCompletion: false)
            bottomNode.layer.animatePosition(from: CGPoint(), to: CGPoint(x: 0.0, y: 30.0), duration: 0.4, delay: 0.0, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false, additive: true)
        }
    }
    
    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        return true
    }
    
    func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        self.interaction?.dismissInput()
    }
    
    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        self.updateAbsoluteRects()
    }
    
    func scrollToTop(animated: Bool) {
        self.scrollNode.view.setContentOffset(CGPoint(), animated: animated)
    }
    
    func updateAbsoluteRects() {
        guard let messageNodes = self.messageNodes, let (size, _, _, _, _, _, _) = self.validLayout else {
            return
        }
        
        for itemNode in messageNodes {
            var absoluteRect = itemNode.frame
            if let supernode = self.supernode {
                absoluteRect = supernode.convert(itemNode.bounds, from: itemNode)
            }
            absoluteRect.origin.y = size.height - absoluteRect.origin.y - absoluteRect.size.height
            itemNode.updateAbsoluteRect(absoluteRect, within: self.bounds.size)
        }
        
        for (_, itemNode) in self.backgroundNodes {
            var absoluteRect = itemNode.frame
            if let supernode = self.supernode {
                absoluteRect = supernode.convert(itemNode.bounds, from: itemNode)
            }
            absoluteRect.origin.y = size.height - absoluteRect.origin.y - absoluteRect.size.height
            itemNode.updateAbsoluteRect(absoluteRect, within: self.bounds.size)
        }
    }
    
    private func beginReordering(itemNode: MediaPickerSelectedItemNode) {
        self.isReordering = true
        
        if let reorderNode = self.reorderNode {
            reorderNode.removeFromSupernode()
        }
        
        let reorderNode = ReorderingItemNode(itemNode: itemNode, initialLocation: itemNode.frame.origin)
        self.reorderNode = reorderNode
        self.scrollNode.addSubnode(reorderNode)
        
        itemNode.isHidden = true
        
        if self.reorderFeedback == nil {
            self.reorderFeedback = HapticFeedback()
        }
        self.reorderFeedback?.impact()
    }
    
    private func endReordering(point: CGPoint?) {
        if let reorderNode = self.reorderNode {
            self.reorderNode = nil
        
            if let itemNode = reorderNode.itemNode, let point = point {
                var targetNode: MediaPickerSelectedItemNode?
                for (_, node) in self.itemNodes {
                    if node.frame.contains(point) {
                        targetNode = node
                        break
                    }
                }
                
                if let targetNode = targetNode, let sourceItem = itemNode.asset as? TGMediaSelectableItem, let targetItem = targetNode.asset as? TGMediaSelectableItem, let targetIndex = self.interaction?.selectionState?.index(of: targetItem) {
                    self.interaction?.selectionState?.move(sourceItem, to: targetIndex)
                }
                reorderNode.animateCompletion(completion: { [weak reorderNode] in
                    reorderNode?.removeFromSupernode()
                })
                self.reorderFeedback?.tap()
            } else {
                reorderNode.removeFromSupernode()
                reorderNode.itemNode?.isHidden = false
            }
        }
        
        self.isReordering = false
    }
    
    private func updateReordering(offset: CGPoint) {
        if let reorderNode = self.reorderNode {
            reorderNode.updateOffset(offset: offset)
        }
    }
    
    private var messageNodes: [ListViewItemNode]?
    private func updateItems(transition: ContainedViewLayoutTransition) {
        guard let (size, insets, items, grouped, theme, wallpaper, bubbleCorners) = self.validLayout else {
            return
        }
        
        let presentationData = self.context.sharedContext.currentPresentationData.with { $0 }
        
        var itemSizes: [CGSize] = []
        let sideInset: CGFloat = 34.0
        let boundingWidth = min(320.0, size.width - insets.left - insets.right - sideInset * 2.0)
        
        var validIds: [String] = []
        for item in items {
            guard let asset = item as? TGMediaEditableItem, let identifier = asset.uniqueIdentifier else {
                continue
            }
            
            validIds.append(identifier)
            let itemNode: MediaPickerSelectedItemNode
            if let current = self.itemNodes[identifier] {
                itemNode = current
            } else {
                itemNode = MediaPickerSelectedItemNode(asset: asset, interaction: self.interaction, enableAnimations: self.context.sharedContext.energyUsageSettings.fullTranslucency)
                self.itemNodes[identifier] = itemNode
                self.scrollNode.addSubnode(itemNode)
                
                itemNode.setup(size: CGSize(width: boundingWidth, height: boundingWidth))
            }
            itemNode.update(theme: theme)
            itemNode.updateSelectionState()
            if !self.isReordering {
                itemNode.updateHiddenMedia()
            }
            
            if let adjustments = self.interaction?.editingState.adjustments(for: asset), adjustments.cropApplied(forAvatar: false) {
                itemSizes.append(adjustments.cropRect.size)
            } else {
                itemSizes.append(asset.originalSize ?? CGSize())
            }
        }
        
        if !self.didSetReady {
            self.didSetReady = true
            
            var signals: [Signal<Bool, NoError>] = []
            for (_, itemNode) in self.itemNodes {
                signals.append(itemNode.ready)
            }
            self.ready.set(combineLatest(queue: Queue.mainQueue(), signals)
            |> map { _ in
                return true
            })
        }
                
        let boundingSize = CGSize(width: boundingWidth, height: boundingWidth)
        var groupLayouts: [([(TGMediaSelectableItem, CGRect, MosaicItemPosition)], CGSize)] = []
        if grouped && items.count > 1 {
            let groupSize = 10
            for i in stride(from: 0, to: itemSizes.count, by: groupSize) {
                let sizes = itemSizes[i ..< min(i + groupSize, itemSizes.count)]
                let items = items[i ..< min(i + groupSize, items.count)]
                
                if items.count > 1 {
                    let (mosaicLayout, size) = chatMessageBubbleMosaicLayout(maxSize: boundingSize, itemSizes: Array(sizes), spacing: 1.0, fillWidth: true)
                    let layout = zip(items, mosaicLayout).map { ($0, $1.0, $1.1) }
                    groupLayouts.append((layout, size))
                } else if let item = items.first, var itemSize = sizes.first {
                    if itemSize.width > itemSize.height {
                        itemSize = itemSize.aspectFitted(boundingSize)
                    } else {
                        itemSize = boundingSize
                    }
                    let itemRect = CGRect(origin: CGPoint(), size: itemSize)
                    let position: MosaicItemPosition = [.top, .bottom, .left, .right]
                    groupLayouts.append(([(item, itemRect, position)], itemRect.size))
                }
            }
        } else {
            for i in 0 ..< itemSizes.count {
                let item = items[i]
                var itemSize = itemSizes[i]
                if itemSize.width > itemSize.height {
                    itemSize = itemSize.aspectFitted(boundingSize)
                } else {
                    itemSize = boundingSize
                }
                let itemRect = CGRect(origin: CGPoint(), size: itemSize)
                let position: MosaicItemPosition = [.top, .bottom, .left, .right]
                groupLayouts.append(([(item, itemRect, position)], itemRect.size))
            }
        }
        
        let peerId = PeerId(namespace: Namespaces.Peer.CloudUser, id: PeerId.Id._internalFromInt64Value(1))
        var peers = SimpleDictionary<PeerId, Peer>()
        peers[peerId] = TelegramUser(id: peerId, accessHash: nil, firstName: "", lastName: "", username: nil, phone: nil, photo: [], botInfo: nil, restrictionInfo: nil, flags: [], emojiStatus: nil, usernames: [], storiesHidden: nil)
        
        let previewText = groupLayouts.count > 1 ? presentationData.strings.Attachment_MessagesPreview : presentationData.strings.Attachment_MessagePreview
        
        let previewMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: previewText, entities: []))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
        let previewItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [previewMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, isCentered: true)
        
        let dragMessage = Message(stableId: 0, stableVersion: 0, id: MessageId(peerId: peerId, namespace: 0, id: 0), globallyUniqueId: nil, groupingKey: nil, groupInfo: nil, threadId: nil, timestamp: 0, flags: [], tags: [], globalTags: [], localTags: [], forwardInfo: nil, author: peers[peerId], text: "", attributes: [], media: [TelegramMediaAction(action: .customText(text: presentationData.strings.Attachment_DragToReorder, entities: []))], peers: peers, associatedMessages: SimpleDictionary(), associatedMessageIds: [], associatedMedia: [:], associatedThreadInfo: nil, associatedStories: [:])
        let dragItem = self.context.sharedContext.makeChatMessagePreviewItem(context: context, messages: [dragMessage], theme: theme, strings: presentationData.strings, wallpaper: wallpaper, fontSize: presentationData.chatFontSize, chatBubbleCorners: bubbleCorners, dateTimeFormat: presentationData.dateTimeFormat, nameOrder: presentationData.nameDisplayOrder, forcedResourceStatus: nil, tapMessage: nil, clickThroughMessage: nil, backgroundNode: self.wallpaperBackgroundNode, availableReactions: nil, isCentered: true)
        
        let headerItems: [ListViewItem] = [previewItem, dragItem]
        
        let params = ListViewItemLayoutParams(width: size.width, leftInset: insets.left, rightInset: insets.right, availableHeight: size.height)
        if let messageNodes = self.messageNodes {
            for i in 0 ..< headerItems.count {
                let itemNode = messageNodes[i]
                headerItems[i].updateNode(async: { $0() }, node: {
                    return itemNode
                }, params: params, previousItem: nil, nextItem: nil, animation: .None, completion: { (layout, apply) in
                    let nodeFrame = CGRect(origin: itemNode.frame.origin, size: CGSize(width: size.width, height: layout.size.height))
                    
                    itemNode.contentSize = layout.contentSize
                    itemNode.insets = layout.insets
                    itemNode.frame = nodeFrame
                    itemNode.isUserInteractionEnabled = false
                    
                    apply(ListViewItemApply(isOnScreen: true))
                })
            }
        } else {
            var messageNodes: [ListViewItemNode] = []
            for i in 0 ..< headerItems.count {
                var itemNode: ListViewItemNode?
                headerItems[i].nodeConfiguredForParams(async: { $0() }, params: params, synchronousLoads: false, previousItem: nil, nextItem: nil, completion: { node, apply in
                    itemNode = node
                    apply().1(ListViewItemApply(isOnScreen: true))
                })
                itemNode!.subnodeTransform = CATransform3DMakeRotation(CGFloat.pi, 0.0, 0.0, 1.0)
                itemNode!.isUserInteractionEnabled = false
                messageNodes.append(itemNode!)
                self.scrollNode.addSubnode(itemNode!)
            }
            self.messageNodes = messageNodes
        }
        
        let spacing: CGFloat = 8.0
        var contentHeight: CGFloat = 60.0
        
        if let previewNode = self.messageNodes?.first {
            transition.updateFrame(node: previewNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + 28.0), size: previewNode.frame.size))
            
            var previewNodeFrame = previewNode.frame
            previewNodeFrame.origin.y = size.height - previewNodeFrame.origin.y - previewNodeFrame.size.height
            
            previewNode.updateFrame(previewNodeFrame, within: size, updateFrame: false)
        }
        
        let graphics = PresentationResourcesChat.principalGraphics(theme: theme, wallpaper: wallpaper, bubbleCorners: bubbleCorners)
        
        var groupIndex = 0
        for (items, groupSize) in groupLayouts {
            let groupRect = CGRect(origin: CGPoint(x: insets.left + floorToScreenPixels((size.width - insets.left - insets.right - groupSize.width) / 2.0), y: insets.top + contentHeight), size: groupSize)
            
            let groupBackgroundNode: MessageBackgroundNode
            if let current = self.backgroundNodes[groupIndex] {
                groupBackgroundNode = current
            } else {
                groupBackgroundNode = MessageBackgroundNode()
                groupBackgroundNode.displaysAsynchronously = false
                self.backgroundNodes[groupIndex] = groupBackgroundNode
                self.scrollNode.insertSubnode(groupBackgroundNode, at: 0)
            }
            
            var itemTransition = transition
            if groupBackgroundNode.frame.width.isZero {
                itemTransition = .immediate
            }

            itemTransition.updateFrame(node: groupBackgroundNode, frame: groupRect.insetBy(dx: -5.0, dy: -2.0).offsetBy(dx: 3.0, dy: 0.0))
            groupBackgroundNode.update(size: groupBackgroundNode.frame.size, theme: theme, wallpaper: wallpaper, graphics: graphics, wallpaperBackgroundNode: self.wallpaperBackgroundNode, transition: itemTransition)
            
            for (item, itemRect, itemPosition) in items {
                if let identifier = item.uniqueIdentifier, let itemNode = self.itemNodes[identifier] {
                    var corners: CACornerMask = []
                    if itemPosition.contains(.top) && itemPosition.contains(.left) {
                        corners.insert(.layerMinXMinYCorner)
                    }
                    if itemPosition.contains(.top) && itemPosition.contains(.right) {
                        corners.insert(.layerMaxXMinYCorner)
                    }
                    if itemPosition.contains(.bottom) && itemPosition.contains(.left) {
                        corners.insert(.layerMinXMaxYCorner)
                    }
                    if itemPosition.contains(.bottom) && itemPosition.contains(.right) {
                        corners.insert(.layerMaxXMaxYCorner)
                    }
                    itemNode.corners = corners
                    itemNode.radius = bubbleCorners.mainRadius
                    
                    var itemTransition = itemTransition
                    if itemNode.frame.width.isZero {
                        itemTransition = .immediate
                    }
                    
                    itemNode.updateLayout(size: itemRect.size, transition: itemTransition)
                    itemTransition.updateFrame(node: itemNode, frame: itemRect.offsetBy(dx: groupRect.minX, dy: groupRect.minY))
                    
                    if case .immediate = itemTransition, case .animated = transition {
                        transition.animateTransformScale(node: itemNode, from: 0.01)
                        itemNode.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.2)
                    }
                }
            }
            
            contentHeight += groupSize.height + spacing
            groupIndex += 1
        }
        
        if let dragNode = self.messageNodes?.last {
            transition.updateAlpha(node: dragNode, alpha: items.count > 1 ? 1.0 : 0.0)
            transition.updateFrame(node: dragNode, frame: CGRect(origin: CGPoint(x: 0.0, y: insets.top + contentHeight + 1.0), size: dragNode.frame.size))
            
            var dragNodeFrame = dragNode.frame
            dragNodeFrame.origin.y = size.height - dragNodeFrame.origin.y - dragNodeFrame.size.height
            
            dragNode.updateFrame(dragNodeFrame, within: size, updateFrame: false)
            contentHeight += 60.0
        }
        
        contentHeight += insets.top
        contentHeight += insets.bottom
        
        var removeIds: [String] = []
        for id in self.itemNodes.keys {
            if !validIds.contains(id) {
                removeIds.append(id)
            }
        }
        for id in removeIds {
            if let itemNode = self.itemNodes.removeValue(forKey: id) {
                if transition.isAnimated {
                    itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
                    itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak itemNode] _ in
                        itemNode?.removeFromSupernode()
                    })
                } else {
                    itemNode.removeFromSupernode()
                }
            }
        }
        
        for id in self.backgroundNodes.keys {
            if id > groupLayouts.count - 1 {
                if let itemNode = self.backgroundNodes.removeValue(forKey: id) {
                    if transition.isAnimated {
                        itemNode.layer.animateScale(from: 1.0, to: 0.1, duration: 0.2, removeOnCompletion: false)
                        itemNode.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.15, removeOnCompletion: false, completion: { [weak itemNode] _ in
                            itemNode?.removeFromSupernode()
                        })
                    } else {
                        itemNode.removeFromSupernode()
                    }
                }
            }
        }
        
        if case let .animated(duration, curve) = transition, self.scrollNode.view.contentSize.height > contentHeight {
            let maxContentOffset = max(0.0, contentHeight - self.scrollNode.frame.height)
            if self.scrollNode.view.contentOffset.y > maxContentOffset {
                let updatedBounds = CGRect(origin: CGPoint(x: 0.0, y: maxContentOffset), size: self.scrollNode.bounds.size)
                let previousBounds = self.scrollNode.bounds
                self.scrollNode.bounds = updatedBounds
                self.scrollNode.layer.animateBounds(from: previousBounds, to: updatedBounds, duration: duration, timingFunction: curve.timingFunction)
            }
        }
        
        self.updateAbsoluteRects()
        
        self.scrollNode.view.contentSize = CGSize(width: size.width, height: contentHeight)
    }
    
    func updateSelectionState() {
        for (_, itemNode) in self.itemNodes {
            itemNode.updateSelectionState()
        }
    }
    
    func updateHiddenMedia() {
        for (_, itemNode) in self.itemNodes {
            itemNode.updateHiddenMedia()
        }
    }
    
    func updateLayout(size: CGSize, insets: UIEdgeInsets, items: [TGMediaSelectableItem], grouped: Bool, theme: PresentationTheme, wallpaper: TelegramWallpaper, bubbleCorners: PresentationChatBubbleCorners, transition: ContainedViewLayoutTransition) {
        let previous = self.validLayout
        self.validLayout = (size, insets, items, grouped, theme, wallpaper, bubbleCorners)
        
        if previous?.theme !== theme || previous?.wallpaper != wallpaper || previous?.bubbleCorners != bubbleCorners {
            self.graphics = PresentationResourcesChat.principalGraphics(theme: theme, wallpaper: wallpaper, bubbleCorners: bubbleCorners)
        }
        
        var itemsTransition = transition
        if previous?.grouped != grouped {
            if let snapshotView = self.view.snapshotView(afterScreenUpdates: false) {
                self.view.addSubview(snapshotView)
                
                snapshotView.layer.animateAlpha(from: 1.0, to: 0.0, duration: 0.2, removeOnCompletion: false, completion: { [weak snapshotView] _ in
                    snapshotView?.removeFromSuperview()
                })
            }
            itemsTransition = .immediate
        }
        
        let inset: CGFloat = insets.left == 70 ? insets.left : 0.0
        self.wallpaperBackgroundNode.update(wallpaper: wallpaper)
        self.wallpaperBackgroundNode.updateBubbleTheme(bubbleTheme: theme, bubbleCorners: bubbleCorners)
        transition.updateFrame(node: self.wallpaperBackgroundNode, frame: CGRect(origin: CGPoint(x: inset, y: 0.0), size: CGSize(width: size.width - inset * 2.0, height: size.height)))
        self.wallpaperBackgroundNode.updateLayout(size: CGSize(width: size.width - inset * 2.0, height: size.height), displayMode: .aspectFill, transition: transition)
        
        self.updateItems(transition: itemsTransition)
        
        let bounds = CGRect(origin: CGPoint(), size: size)
        transition.updateFrame(node: self.scrollNode, frame: bounds)
    }
    
    func transitionView(for identifier: String, hideSource: Bool) -> UIView? {
        if hideSource {
            for (_, node) in self.backgroundNodes {
                node.alpha = 0.01
            }
        }
        for (_, itemNode) in self.itemNodes {
            if itemNode.asset.uniqueIdentifier == identifier {
                if hideSource {
                    itemNode.alpha = 0.01
                }
                return itemNode.transitionView()
            }
        }
        return nil
    }
}

private class ReorderingGestureRecognizer: UIGestureRecognizer {
    private let shouldBegin: (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: MediaPickerSelectedItemNode?)
    private let willBegin: (CGPoint) -> Void
    private let began: (MediaPickerSelectedItemNode) -> Void
    private let ended: (CGPoint?) -> Void
    private let moved: (CGPoint) -> Void
    
    private var initialLocation: CGPoint?
    private var longPressTimer: SwiftSignalKit.Timer?
    
    var animateOnTouch = true
    
    private var itemNode: MediaPickerSelectedItemNode?
    
    public init(animateOnTouch: Bool, shouldBegin: @escaping (CGPoint) -> (allowed: Bool, requiresLongPress: Bool, itemNode: MediaPickerSelectedItemNode?), willBegin: @escaping (CGPoint) -> Void, began: @escaping (MediaPickerSelectedItemNode) -> Void, ended: @escaping (CGPoint?) -> Void, moved: @escaping (CGPoint) -> Void) {
        self.animateOnTouch = animateOnTouch
        self.shouldBegin = shouldBegin
        self.willBegin = willBegin
        self.began = began
        self.ended = ended
        self.moved = moved
        
        super.init(target: nil, action: nil)
    }
    
    deinit {
        self.longPressTimer?.invalidate()
    }
    
    private func startLongPressTimer() {
        self.longPressTimer?.invalidate()
        let longPressTimer = SwiftSignalKit.Timer(timeout: 0.3, repeat: false, completion: { [weak self] in
            self?.longPressTimerFired()
        }, queue: Queue.mainQueue())
        self.longPressTimer = longPressTimer
        longPressTimer.start()
    }
    
    private func stopLongPressTimer() {
        self.itemNode = nil
        self.longPressTimer?.invalidate()
        self.longPressTimer = nil
    }
    
    override public func reset() {
        super.reset()
        
        self.itemNode = nil
        self.stopLongPressTimer()
        self.initialLocation = nil
    }
    
 
    private func longPressTimerFired() {
        guard let _ = self.initialLocation else {
            return
        }
        
        self.state = .began
        self.longPressTimer?.invalidate()
        self.longPressTimer = nil
        if let itemNode = self.itemNode {
            self.began(itemNode)
        }
    }
    
    private var currentItemNode: ASDisplayNode?
    override public func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesBegan(touches, with: event)
        
        if self.numberOfTouches > 1 {
            self.state = .failed
            self.ended(nil)
            return
        }
        
        if self.state == .possible {
            if let location = touches.first?.location(in: self.view) {
                let (allowed, requiresLongPress, itemNode) = self.shouldBegin(location)
                if allowed {
                    if let itemNode = itemNode, self.animateOnTouch {
                        itemNode.layer.animateScale(from: 1.0, to: 0.98, duration: 0.2, delay: 0.1)
                    }
                    self.itemNode = itemNode
                    self.initialLocation = location
                    if requiresLongPress {
                        self.startLongPressTimer()
                    } else {
                        self.state = .began
                        if let itemNode = self.itemNode {
                            self.began(itemNode)
                        }
                    }
                } else {
                    self.state = .failed
                }
            } else {
                self.state = .failed
            }
        }
    }
    
    override public func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesEnded(touches, with: event)
        
        self.initialLocation = nil
        
        if self.longPressTimer != nil {
            self.stopLongPressTimer()
            self.state = .failed
        }
        if self.state == .began || self.state == .changed {
            if let location = touches.first?.location(in: self.view) {
                self.ended(location)
            } else {
                self.ended(nil)
            }
            self.state = .failed
        }
    }
    
    override public func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesCancelled(touches, with: event)
        
        self.initialLocation = nil
        
        if self.longPressTimer != nil {
            self.stopLongPressTimer()
            self.state = .failed
        }
        if self.state == .began || self.state == .changed {
            self.ended(nil)
            self.state = .failed
        }
    }
    
    override public func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent) {
        super.touchesMoved(touches, with: event)
        
        if (self.state == .began || self.state == .changed), let initialLocation = self.initialLocation, let location = touches.first?.location(in: self.view) {
            self.state = .changed
            self.moved(CGPoint(x: location.x - initialLocation.x, y: location.y - initialLocation.y))
        } else if let touch = touches.first, let initialTapLocation = self.initialLocation, self.longPressTimer != nil {
            let touchLocation = touch.location(in: self.view)
            let dX = touchLocation.x - initialTapLocation.x
            let dY = touchLocation.y - initialTapLocation.y
            
            if dX * dX + dY * dY > 3.0 * 3.0 {
                self.itemNode?.layer.removeAllAnimations()
                
                self.stopLongPressTimer()
                self.initialLocation = nil
                self.state = .failed
            }
        }
    }
}


private func generateShadowImage(corners: CACornerMask, radius: CGFloat) -> UIImage? {
    return generateImage(CGSize(width: 120.0, height: 120), rotatedContext: { size, context in
        context.clear(CGRect(origin: CGPoint(), size: size))
        
//        context.saveGState()
        context.setShadow(offset: CGSize(), blur: 28.0, color: UIColor(white: 0.0, alpha: 0.4).cgColor)

        var rectCorners: UIRectCorner = []
        if corners.contains(.layerMinXMinYCorner) {
            rectCorners.insert(.topLeft)
        }
        if corners.contains(.layerMaxXMinYCorner) {
            rectCorners.insert(.topRight)
        }
        if corners.contains(.layerMinXMaxYCorner) {
            rectCorners.insert(.bottomLeft)
        }
        if corners.contains(.layerMaxXMaxYCorner) {
            rectCorners.insert(.bottomRight)
        }
        
        let path = UIBezierPath(roundedRect: CGRect(x: 30.0, y: 30.0, width: 60.0, height: 60.0), byRoundingCorners: rectCorners, cornerRadii: CGSize(width: radius, height: radius)).cgPath
        context.addPath(path)
        context.fillPath()
//        context.restoreGState()
        
//        context.setBlendMode(.clear)
//        context.addPath(path)
//        context.fillPath()
    })?.stretchableImage(withLeftCapWidth: 60, topCapHeight: 60)
}

private final class CopyView: UIView {
    let shadow: UIImageView
    var snapshotView: UIView?
    
    init(frame: CGRect, corners: CACornerMask, radius: CGFloat) {
        self.shadow = UIImageView()
        self.shadow.contentMode = .scaleToFill
        
        super.init(frame: frame)
    
        self.shadow.image = generateShadowImage(corners: corners, radius: radius)
    
        self.addSubview(self.shadow)
    }
    
    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
}

private final class ReorderingItemNode: ASDisplayNode {
    weak var itemNode: MediaPickerSelectedItemNode?
    
    var currentState: (Int, Int)?
    
    private let copyView: CopyView
    private let initialLocation: CGPoint
    
    init(itemNode: MediaPickerSelectedItemNode, initialLocation: CGPoint) {
        self.itemNode = itemNode
        self.copyView = CopyView(frame: CGRect(), corners: itemNode.corners, radius: itemNode.radius)
        let snapshotView = itemNode.view.snapshotView(afterScreenUpdates: false)
        self.initialLocation = initialLocation
        
        super.init()
        
        if let snapshotView = snapshotView {
            snapshotView.frame = CGRect(origin: CGPoint(), size: itemNode.bounds.size)
            snapshotView.bounds.origin = itemNode.bounds.origin
            self.copyView.addSubview(snapshotView)
            self.copyView.snapshotView = snapshotView
        }
        self.view.addSubview(self.copyView)
        self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x, y: initialLocation.y), size: itemNode.bounds.size)
        self.copyView.shadow.frame = CGRect(origin: CGPoint(x: -30.0, y: -30.0), size: CGSize(width: itemNode.bounds.size.width + 60.0, height: itemNode.bounds.size.height + 60.0))
        self.copyView.shadow.layer.animateAlpha(from: 0.0, to: 1.0, duration: 0.25)
        
        self.copyView.snapshotView?.layer.animateScale(from: 1.0, to: 1.05, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
        self.copyView.shadow.layer.animateScale(from: 1.0, to: 1.05, duration: 0.2, timingFunction: kCAMediaTimingFunctionSpring, removeOnCompletion: false)
    }
    
    func updateOffset(offset: CGPoint) {
        self.copyView.frame = CGRect(origin: CGPoint(x: initialLocation.x + offset.x, y: initialLocation.y + offset.y), size: copyView.bounds.size)
    }
    
    func currentOffset() -> CGFloat? {
        return self.copyView.center.y
    }
    
    func animateCompletion(completion: @escaping () -> Void) {
        if let itemNode = self.itemNode {
            itemNode.view.superview?.bringSubviewToFront(itemNode.view)
                        
            itemNode.layer.animateScale(from: 1.05, to: 1.0, duration: 0.25, removeOnCompletion: false)
            
            let sourceFrame = self.view.convert(self.copyView.frame, to: itemNode.supernode?.view)
            let targetFrame = itemNode.frame
            itemNode.updateLayout(size: sourceFrame.size, transition: .immediate)
            itemNode.layer.animateFrame(from: sourceFrame, to: targetFrame, duration: 0.3, timingFunction: kCAMediaTimingFunctionSpring, completion: { _ in
                completion()
            })
            itemNode.updateLayout(size: targetFrame.size, transition: .animated(duration: 0.3, curve: .spring))
            
            itemNode.isHidden = false
            self.copyView.isHidden = true
        } else {
            completion()
        }
    }
}