import Foundation
import UIKit
import Display
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import TelegramCore
import AVFoundation
import ContextUI
import TelegramPresentationData
import ShimmerEffect
import SoftwareVideo

final class MultiplexedVideoPlaceholderNode: ASDisplayNode {
    private let effectNode: ShimmerEffectNode
    private var theme: PresentationTheme?
    private var size: CGSize?
    
    override init() {
        self.effectNode = ShimmerEffectNode()
        
        super.init()
        
        self.addSubnode(self.effectNode)
    }
    
    func update(size: CGSize, theme: PresentationTheme) {
        if self.theme === theme && self.size == size {
            return
        }
        
        self.effectNode.frame = CGRect(origin: CGPoint(), size: size)
        self.effectNode.update(backgroundColor: theme.chat.inputMediaPanel.stickersBackgroundColor.withAlphaComponent(1.0), foregroundColor: theme.chat.inputMediaPanel.stickersSectionTextColor.blitOver(theme.chat.inputMediaPanel.stickersBackgroundColor, alpha: 0.2), shimmeringColor: theme.list.itemBlocksBackgroundColor.withAlphaComponent(0.3), shapes: [.rect(rect: CGRect(origin: CGPoint(), size: size))], size: bounds.size)
    }
    
    func updateAbsoluteRect(_ absoluteRect: CGRect, within containerSize: CGSize) {
        self.effectNode.updateAbsoluteRect(absoluteRect, within: containerSize)
    }
}

private final class MultiplexedVideoTrackingNode: ASDisplayNode {
    var inHierarchyUpdated: ((Bool) -> Void)?
    
    override func willEnterHierarchy() {
        super.willEnterHierarchy()
        
        self.inHierarchyUpdated?(true)
    }
    
    override func didExitHierarchy() {
        super.didExitHierarchy()
        
        self.inHierarchyUpdated?(false)
    }
}

private final class VisibleVideoItem {
    enum Id: Equatable, Hashable {
        case saved(MediaId)
        case trending(MediaId)
    }
    let id: Id
    let file: MultiplexedVideoNodeFile
    let frame: CGRect
    
    init(file: MultiplexedVideoNodeFile, frame: CGRect, isTrending: Bool) {
        self.file = file
        self.frame = frame
        if isTrending {
            self.id = .trending(file.file.media.fileId)
        } else {
            self.id = .saved(file.file.media.fileId)
        }
    }
}

public final class MultiplexedVideoNodeFile {
    public let file: FileMediaReference
    public let contextResult: (ChatContextResultCollection, ChatContextResult)?
    
    public init(file: FileMediaReference, contextResult: (ChatContextResultCollection, ChatContextResult)?) {
        self.file = file
        self.contextResult = contextResult
    }
}

public final class MultiplexedVideoNodeFiles {
    public let saved: [MultiplexedVideoNodeFile]
    public let trending: [MultiplexedVideoNodeFile]
    public let isSearch: Bool
    public let canLoadMore: Bool
    public let isStale: Bool
    
    public init(saved: [MultiplexedVideoNodeFile], trending: [MultiplexedVideoNodeFile], isSearch: Bool, canLoadMore: Bool, isStale: Bool) {
        self.saved = saved
        self.trending = trending
        self.isSearch = isSearch
        self.canLoadMore = canLoadMore
        self.isStale = isStale
    }
}

public final class MultiplexedVideoNode: ASDisplayNode, ASScrollViewDelegate {
    private let account: Account
    private var theme: PresentationTheme
    private var strings: PresentationStrings
    private let trackingNode: MultiplexedVideoTrackingNode
    
    public var didScroll: ((CGFloat, CGFloat) -> Void)?
    public var didEndScrolling: (() -> Void)?
    public var reactionSelected: ((String) -> Void)?
    
    public var topInset: CGFloat = 0.0 {
        didSet {
            self.setNeedsLayout()
        }
    }
    
    public var bottomInset: CGFloat = 0.0 {
        didSet {
            self.setNeedsLayout()
        }
    }
    
    public var idealHeight: CGFloat = 93.0 {
        didSet {
            self.setNeedsLayout()
        }
    }
    
    public private(set) var files: MultiplexedVideoNodeFiles = MultiplexedVideoNodeFiles(saved: [], trending: [], isSearch: false, canLoadMore: false, isStale: false)
    
    public func setFiles(files: MultiplexedVideoNodeFiles, synchronous: Bool, resetScrollingToOffset: CGFloat?) {
        self.files = files
        
        self.ignoreDidScroll = true
        if let resetScrollingToOffset = resetScrollingToOffset {
            self.scrollNode.view.contentOffset = CGPoint(x: 0.0, y :resetScrollingToOffset)
        }
        self.updateVisibleItems(extendSizeForTransition: 0.0, transition: .immediate, synchronous: synchronous)
        self.ignoreDidScroll = false
    }
    
    private var displayItems: [VisibleVideoItem] = []
    private var visibleThumbnailLayers: [VisibleVideoItem.Id: SoftwareVideoThumbnailNode] = [:]
    private var visiblePlaceholderNodes: [Int: MultiplexedVideoPlaceholderNode] = [:]

    private let contextContainerNode: ContextControllerSourceNode
    public let scrollNode: ASScrollNode
    
    private var visibleLayers: [VisibleVideoItem.Id: (SoftwareVideoLayerFrameManager, SampleBufferLayer)] = [:]
    
    private let trendingTitleNode: ImmediateTextNode
    
    private var displayLink: CADisplayLink!
    private var timeOffset = 0.0
    private var pauseTime = 0.0
    
    private let timebase: CMTimebase
    
    public var fileSelected: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect) -> Void)?
    public var fileContextMenu: ((MultiplexedVideoNodeFile, ASDisplayNode, CGRect, ContextGesture, Bool) -> Void)?
    public var enableVideoNodes = false
        
    private var currentActivatingId: VisibleVideoItem.Id?
    private var isFinishingActivation = false
    
    public init(account: Account, theme: PresentationTheme, strings: PresentationStrings) {
        self.account = account
        self.theme = theme
        self.strings = strings
        self.trackingNode = MultiplexedVideoTrackingNode()
        self.trackingNode.isLayerBacked = true
        
        var timebase: CMTimebase?
        CMTimebaseCreateWithSourceClock(allocator: nil, sourceClock: CMClockGetHostTimeClock(), timebaseOut: &timebase)
        CMTimebaseSetRate(timebase!, rate: 0.0)
        self.timebase = timebase!
        
        self.contextContainerNode = ContextControllerSourceNode()
        self.contextContainerNode.animateScale = false
        self.scrollNode = ASScrollNode()
        
        self.trendingTitleNode = ImmediateTextNode()
        self.trendingTitleNode.attributedText = NSAttributedString(string: strings.Chat_Gifs_TrendingSectionHeader, font: Font.medium(12.0), textColor: theme.chat.inputMediaPanel.stickersSectionTextColor)
        
        super.init()
        
        self.isOpaque = true
        self.scrollNode.view.showsVerticalScrollIndicator = false
        self.scrollNode.view.showsHorizontalScrollIndicator = false
        self.scrollNode.view.alwaysBounceVertical = true
        
        self.scrollNode.addSubnode(self.trendingTitleNode)
        
        self.addSubnode(self.trackingNode)
        self.addSubnode(self.contextContainerNode)
        self.contextContainerNode.addSubnode(self.scrollNode)
        
        class DisplayLinkProxy: NSObject {
            weak var target: MultiplexedVideoNode?
            
            init(target: MultiplexedVideoNode) {
                self.target = target
            }
            
            @objc func displayLinkEvent() {
                self.target?.displayLinkEvent()
            }
        }
        
        self.displayLink = CADisplayLink(target: DisplayLinkProxy(target: self), selector: #selector(DisplayLinkProxy.displayLinkEvent))
        self.displayLink.add(to: RunLoop.main, forMode: .common)
        if #available(iOS 10.0, *) {
            self.displayLink.preferredFramesPerSecond = 25
        } else {
            self.displayLink.frameInterval = 2
        }
        self.displayLink.isPaused = true
        
        self.trackingNode.inHierarchyUpdated = { [weak self] value in
            if let strongSelf = self {
                if !value {
                    CMTimebaseSetRate(strongSelf.timebase, rate: 0.0)
                } else {
                    CMTimebaseSetRate(strongSelf.timebase, rate: 1.0)
                }
                strongSelf.displayLink.isPaused = !value
                if value && !strongSelf.enableVideoNodes {
                    strongSelf.enableVideoNodes = true
                    strongSelf.validVisibleItemsOffset = nil
                    strongSelf.updateImmediatelyVisibleItems()
                } else if !value {
                    strongSelf.enableVideoNodes = false
                }
            }
        }
        
        self.scrollNode.view.delegate = self.wrappedScrollViewDelegate
        
        let recognizer = UITapGestureRecognizer(target: self, action: #selector(self.tapGesture(_:)))
        self.view.addGestureRecognizer(recognizer)
        
        var gestureLocation: CGPoint?
        
        self.contextContainerNode.shouldBegin = { [weak self] point in
            guard let strongSelf = self else {
                return false
            }
            gestureLocation = point
            if let (id, _, _, _) = strongSelf.internalFileAt(point: point) {
                strongSelf.currentActivatingId = id
                return true
            } else {
                return false
            }
        }
        
        self.contextContainerNode.customActivationProgress = { [weak self] progress, update in
            guard let strongSelf = self else {
                return
            }
            
            if let currentActivatingId = strongSelf.currentActivatingId, let (_, layer) = strongSelf.visibleLayers[currentActivatingId] {
                let layer = layer.layer
                
                let targetContentRect: CGRect = layer.bounds
                
                let scaleSide = targetContentRect.width
                let minScale: CGFloat = max(0.7, (scaleSide - 15.0) / scaleSide)
                let currentScale = 1.0 * (1.0 - progress) + minScale * progress
                
                let originalCenterOffsetX: CGFloat = targetContentRect.width / 2.0 - targetContentRect.midX
                let scaledCenterOffsetX: CGFloat = originalCenterOffsetX * currentScale
                
                let originalCenterOffsetY: CGFloat = targetContentRect.height / 2.0 - targetContentRect.midY
                let scaledCenterOffsetY: CGFloat = originalCenterOffsetY * currentScale
                
                let scaleMidX: CGFloat = scaledCenterOffsetX - originalCenterOffsetX
                let scaleMidY: CGFloat = scaledCenterOffsetY - originalCenterOffsetY
                
                switch update {
                case .update:
                    let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
                    layer.sublayerTransform = sublayerTransform
                case .begin:
                    let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
                    layer.sublayerTransform = sublayerTransform
                    
                    if let thumbnail = strongSelf.visibleThumbnailLayers[currentActivatingId] {
                        thumbnail.isHidden = true
                    }
                case .ended:
                    if !strongSelf.isFinishingActivation {
                        strongSelf.isFinishingActivation = true
                        let sublayerTransform = CATransform3DTranslate(CATransform3DScale(CATransform3DIdentity, currentScale, currentScale, 1.0), scaleMidX, scaleMidY, 0.0)
                        let previousTransform = layer.sublayerTransform
                        layer.sublayerTransform = sublayerTransform
                        layer.animate(from: NSValue(caTransform3D: previousTransform), to: NSValue(caTransform3D: sublayerTransform), keyPath: "sublayerTransform", timingFunction: CAMediaTimingFunctionName.easeOut.rawValue, duration: 0.2, completion: { _ in
                            strongSelf.isFinishingActivation = false
                            if let thumbnail = strongSelf.visibleThumbnailLayers[currentActivatingId] {
                                thumbnail.isHidden = false
                            }
                        })
                    }
                }
            }
        }
        
        self.contextContainerNode.activated = { [weak self] gesture, _ in
            guard let strongSelf = self, let gestureLocation = gestureLocation else {
                return
            }
            if let (_, file, rect, isSaved) = strongSelf.internalFileAt(point: gestureLocation) {
                if !strongSelf.files.isStale {
                    strongSelf.fileContextMenu?(file, strongSelf, rect.offsetBy(dx: 0.0, dy: -strongSelf.scrollNode.bounds.minY), gesture, isSaved)
                } else {
                    gesture.cancel()
                }
            } else {
                gesture.cancel()
            }
        }
    }
    
    required init?(coder aDecoder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
    
    deinit {
        self.displayLink.invalidate()
        self.displayLink.isPaused = true
        for (_, value) in self.visibleLayers {
            value.1.isFreed = true
        }
        clearSampleBufferLayerPoll()
    }
    
    private func displayLinkEvent() {
        let timestamp = CMTimebaseGetTime(self.timebase).seconds
        for (_, (manager, _)) in self.visibleLayers {
            manager.tick(timestamp: timestamp)
        }
    }
    
    private var validSize: CGSize?
    public func updateLayout(theme: PresentationTheme, strings: PresentationStrings, size: CGSize, transition: ContainedViewLayoutTransition) {
        self.theme = theme
        self.strings = strings
        if self.validSize == nil || !self.validSize!.equalTo(size) {
            let previousSize = self.validSize ?? CGSize()
            self.validSize = size
            self.contextContainerNode.frame = CGRect(origin: CGPoint(), size: size)
            transition.updateFrame(node: self.scrollNode, frame: CGRect(origin: CGPoint(), size: size))
            let startTime = CFAbsoluteTimeGetCurrent()
            self.updateVisibleItems(extendSizeForTransition: max(0.0, previousSize.height - size.height), transition: transition)
            print("MultiplexedVideoNode layout updateVisibleItems: \((CFAbsoluteTimeGetCurrent() - startTime) * 1000.0) ms")
        }
    }
    
    private var ignoreDidScroll: Bool = false
    
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if !self.ignoreDidScroll {
            self.updateImmediatelyVisibleItems()
            self.didScroll?(scrollView.contentOffset.y, scrollView.contentSize.height)
        }
    }
    
    public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        self.didEndScrolling?()
    }
    
    public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if !decelerate {
            self.didEndScrolling?()
        }
    }
    
    private var currentExtendSizeForTransition: CGFloat = 0.0
    
    private var validVisibleItemsOffset: CGFloat?
    private func updateImmediatelyVisibleItems(ensureFrames: Bool = false, synchronous: Bool = false) {
        var visibleBounds = self.scrollNode.bounds
        let containerSize = visibleBounds.size
        visibleBounds.size.height += max(0.0, self.currentExtendSizeForTransition)
        let visibleThumbnailBounds = visibleBounds.insetBy(dx: 0.0, dy: -350.0)
        
        let containerWidth = containerSize.width
        let itemSpacing: CGFloat = 1.0
        let itemsInRow = max(3, min(6, Int(containerWidth / 140.0)))
        let itemSize: CGFloat = floor(containerWidth / CGFloat(itemsInRow))
        
        let absoluteContainerSize = CGSize(width: containerSize.width, height: containerSize.height)
        let absoluteContainerOffset = -visibleBounds.origin.y
        
        if let validVisibleItemsOffset = self.validVisibleItemsOffset, validVisibleItemsOffset.isEqual(to: visibleBounds.origin.y) {
            return
        }
        self.validVisibleItemsOffset = visibleBounds.origin.y
        let minVisibleY = visibleBounds.minY
        let maxVisibleY = visibleBounds.maxY
        
        let minVisibleThumbnailY = visibleThumbnailBounds.minY
        let maxVisibleThumbnailY = visibleThumbnailBounds.maxY
        
        var visibleThumbnailIds = Set<VisibleVideoItem.Id>()
        var visibleIds = Set<VisibleVideoItem.Id>()
        
        var maxVisibleIndex = -1
        
        for index in 0 ..< self.displayItems.count {
            let item = self.displayItems[index]
            
            if item.frame.maxY < minVisibleThumbnailY {
                continue
            }
            if item.frame.minY > maxVisibleThumbnailY {
                break
            }
            
            maxVisibleIndex = max(maxVisibleIndex, index)
            
            visibleThumbnailIds.insert(item.id)
            
            let thumbnailLayer: SoftwareVideoThumbnailNode
            if let current = self.visibleThumbnailLayers[item.id] {
                thumbnailLayer = current
                if ensureFrames {
                    thumbnailLayer.frame = item.frame
                }
            } else {
                var existingPlaceholderNode: MultiplexedVideoPlaceholderNode?
                if let placeholderNode = self.visiblePlaceholderNodes[index] {
                    existingPlaceholderNode = placeholderNode
                    self.visiblePlaceholderNodes.removeValue(forKey: index)
                    placeholderNode.removeFromSupernode()
                }
                
                thumbnailLayer = SoftwareVideoThumbnailNode(account: self.account, fileReference: item.file.file, synchronousLoad: synchronous, usePlaceholder: true, existingPlaceholder: existingPlaceholderNode)
                thumbnailLayer.frame = item.frame
                self.scrollNode.addSubnode(thumbnailLayer)
                self.visibleThumbnailLayers[item.id] = thumbnailLayer
            }
            
            thumbnailLayer.update(theme: self.theme, size: item.frame.size)
            thumbnailLayer.updateAbsoluteRect(item.frame.offsetBy(dx: 0.0, dy: absoluteContainerOffset), within: absoluteContainerSize)
            
            if item.frame.maxY < minVisibleY {
                continue
            }
            if item.frame.minY > maxVisibleY {
                continue
            }
            
            visibleIds.insert(item.id)
            
            if let (_, layerHolder) = self.visibleLayers[item.id] {
                if ensureFrames {
                    layerHolder.layer.frame = item.frame
                }
            } else {
                let layerHolder = takeSampleBufferLayer()
                layerHolder.layer.videoGravity = AVLayerVideoGravity.resizeAspectFill
                layerHolder.layer.frame = item.frame
                self.scrollNode.layer.addSublayer(layerHolder.layer)
                let manager = SoftwareVideoLayerFrameManager(account: self.account, userLocation: .other, userContentType: .other, fileReference: item.file.file, layerHolder: layerHolder)
                self.visibleLayers[item.id] = (manager, layerHolder)
                self.visibleThumbnailLayers[item.id]?.ready = { [weak self] in
                    if let strongSelf = self {
                        strongSelf.visibleLayers[item.id]?.0.start()
                    }
                }
            }
        }
        
        var visiblePlaceholderIndices = Set<Int>()
        if self.files.canLoadMore {
            let verticalOffset: CGFloat = self.topInset
            
            let sideInset: CGFloat = 0.0
            
            var indexImpl = maxVisibleIndex + 1
            while true {
                let index = indexImpl
                indexImpl += 1
                
                let rowIndex = index / Int(itemsInRow)
                let columnIndex = index % Int(itemsInRow)
                let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: verticalOffset + itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing))
                let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (containerWidth - itemOrigin.x) : itemSize, height: itemSize))
                if itemFrame.maxY < minVisibleY {
                    continue
                }
                if itemFrame.minY > maxVisibleY {
                    break
                }
                visiblePlaceholderIndices.insert(index)
                
                let placeholderNode: MultiplexedVideoPlaceholderNode
                if let current = self.visiblePlaceholderNodes[index] {
                    placeholderNode = current
                } else {
                    placeholderNode = MultiplexedVideoPlaceholderNode()
                    self.visiblePlaceholderNodes[index] = placeholderNode
                    self.scrollNode.addSubnode(placeholderNode)
                }
                placeholderNode.frame = itemFrame
                placeholderNode.update(size: itemFrame.size, theme: self.theme)
                placeholderNode.updateAbsoluteRect(itemFrame.offsetBy(dx: 0.0, dy: absoluteContainerOffset), within: absoluteContainerSize)
            }
        }
        
        var removeIds: [VisibleVideoItem.Id] = []
        for id in self.visibleLayers.keys {
            if !visibleIds.contains(id) {
                removeIds.append(id)
            }
        }
        
        var removeThumbnailIds: [VisibleVideoItem.Id] = []
        for id in self.visibleThumbnailLayers.keys {
            if !visibleThumbnailIds.contains(id) {
                removeThumbnailIds.append(id)
            }
        }
        
        var removePlaceholderIndices: [Int] = []
        for index in self.visiblePlaceholderNodes.keys {
            if !visiblePlaceholderIndices.contains(index) {
                removePlaceholderIndices.append(index)
            }
        }
        
        for id in removeIds {
            let (_, layerHolder) = self.visibleLayers[id]!
            layerHolder.layer.removeFromSuperlayer()
            self.visibleLayers.removeValue(forKey: id)
        }
        
        for id in removeThumbnailIds {
            let thumbnailLayer = self.visibleThumbnailLayers[id]!
            thumbnailLayer.removeFromSupernode()
            self.visibleThumbnailLayers.removeValue(forKey: id)
        }
        
        for index in removePlaceholderIndices {
            if let placeholderNode = self.visiblePlaceholderNodes[index] {
                placeholderNode.removeFromSupernode()
                self.visiblePlaceholderNodes.removeValue(forKey: index)
            }
        }
    }
    
    private func updateVisibleItems(extendSizeForTransition: CGFloat, transition: ContainedViewLayoutTransition, synchronous: Bool = false) {
        let drawableSize = self.scrollNode.bounds.size
        if !drawableSize.width.isZero {
            var displayItems: [VisibleVideoItem] = []
            
            var verticalOffset: CGFloat = self.topInset
            
            func commitFileGrid(files: [MultiplexedVideoNodeFile], isTrending: Bool) {
                let containerWidth = drawableSize.width
                let itemCount = files.count
                let itemSpacing: CGFloat = 1.0
                let itemsInRow = max(3, min(6, Int(containerWidth / 140.0)))
                let itemSize: CGFloat = floor(containerWidth / CGFloat(itemsInRow))
                
                let rowCount = itemCount / itemsInRow + (itemCount % itemsInRow == 0 ? 0 : 1)
                
                let sideInset: CGFloat = 0.0
                
                for index in 0 ..< itemCount {
                    let rowIndex = index / Int(itemsInRow)
                    let columnIndex = index % Int(itemsInRow)
                    let itemOrigin = CGPoint(x: sideInset + CGFloat(columnIndex) * (itemSize + itemSpacing), y: verticalOffset + itemSpacing + CGFloat(rowIndex) * (itemSize + itemSpacing))
                    let itemFrame = CGRect(origin: itemOrigin, size: CGSize(width: columnIndex == itemsInRow ? (containerWidth - itemOrigin.x) : itemSize, height: itemSize))
                    displayItems.append(VisibleVideoItem(file: files[index], frame: itemFrame, isTrending: isTrending))
                }
                
                let contentHeight = CGFloat(rowCount + 1) * itemSpacing + CGFloat(rowCount) * itemSize
                verticalOffset += contentHeight
            }
            
            func commitFilesSpans(files: [MultiplexedVideoNodeFile], isTrending: Bool) {
                var rowsCount = 0
                var firstRowMax = 0;
                
                let viewPortAvailableSize = drawableSize.width
                
                let preferredRowSize: CGFloat = 100.0
                let itemsCount = files.count
                let spanCount: CGFloat = 100.0
                var spanLeft = spanCount
                var currentItemsInRow = 0
                var currentItemsSpanAmount: CGFloat = 0.0
                
                var itemSpans: [Int: CGFloat] = [:]
                var itemsToRow: [Int: Int] = [:]
                
                for a in 0 ..< itemsCount {
                    var size: CGSize
                    if let dimensions = files[a].file.media.dimensions {
                        size = dimensions.cgSize
                    } else {
                        size = CGSize(width: 100.0, height: 100.0)
                    }
                    if size.width <= 0.0 {
                        size.width = 100.0
                    }
                    if size.height <= 0.0 {
                        size.height = 100.0
                    }
                    //size = CGSize(width: 100.0, height: 100.0)
                    let aspect: CGFloat = size.width / size.height
                    if aspect > 4.0 || aspect < 0.2 {
                        size.width = max(size.width, size.height)
                        size.height = size.width
                    }

                    var requiredSpan = min(spanCount, floor(spanCount * (size.width / size.height * preferredRowSize / viewPortAvailableSize)))
                    let moveToNewRow = spanLeft < requiredSpan || requiredSpan > 33.0 && spanLeft < requiredSpan - 15.0
                    if moveToNewRow {
                        if spanLeft > 0 {
                            let spanPerItem = floor(spanLeft / CGFloat(currentItemsInRow))
                            
                            let start = a - currentItemsInRow
                            var b = start
                            while b < start + currentItemsInRow {
                                if (b == start + currentItemsInRow - 1) {
                                    itemSpans[b] = itemSpans[b]! + spanLeft
                                } else {
                                    itemSpans[b] = itemSpans[b]! + spanPerItem
                                }
                                spanLeft -= spanPerItem;
                                
                                b += 1
                            }
                            
                            itemsToRow[a - 1] = rowsCount
                        }
                        rowsCount += 1
                        currentItemsSpanAmount = 0
                        currentItemsInRow = 0
                        spanLeft = spanCount
                    } else {
                        if spanLeft < requiredSpan {
                            requiredSpan = spanLeft
                        }
                    }
                    if rowsCount == 0 {
                        firstRowMax = max(firstRowMax, a)
                    }
                    if a == itemsCount - 1 {
                        itemsToRow[a] = rowsCount
                    }
                    currentItemsSpanAmount += requiredSpan
                    currentItemsInRow += 1
                    spanLeft -= requiredSpan
                    spanLeft = max(0, spanLeft)

                    itemSpans[a] = requiredSpan
                }
                if itemsCount != 0 {
                    rowsCount += 1
                }
                
                var currentRowHorizontalOffset: CGFloat = 0.0
                for index in 0 ..< files.count {
                    guard let width = itemSpans[index] else {
                        continue
                    }
                    let itemWidth = floor(width * drawableSize.width / 100.0) - 1
                    
                    var itemSize = CGSize(width: itemWidth, height: preferredRowSize)
                    if itemsToRow[index] != nil && currentRowHorizontalOffset + itemSize.width >= drawableSize.width - 10.0 {
                        itemSize.width = max(itemSize.width, drawableSize.width - currentRowHorizontalOffset)
                    }
                    displayItems.append(VisibleVideoItem(file: files[index], frame: CGRect(origin: CGPoint(x: currentRowHorizontalOffset, y: verticalOffset), size: itemSize), isTrending: isTrending))
                    currentRowHorizontalOffset += itemSize.width + 1.0
                    
                    if itemsToRow[index] != nil {
                        verticalOffset += preferredRowSize + 1.0
                        currentRowHorizontalOffset = 0.0
                    }
                }
            }
            
            var hasContent = false
            if !self.files.saved.isEmpty {
                commitFileGrid(files: self.files.saved, isTrending: false)
                hasContent = true
            }
            if !self.files.trending.isEmpty {
                if self.files.isSearch {
                    self.trendingTitleNode.isHidden = true
                } else {
                    self.trendingTitleNode.isHidden = false
                    if hasContent {
                        verticalOffset += 16.0
                    }
                    let leftInset: CGFloat = 10.0
                    let trendingTitleSize = self.trendingTitleNode.updateLayout(CGSize(width: drawableSize.width - leftInset * 2.0, height: 100.0))
                    self.trendingTitleNode.frame = CGRect(origin: CGPoint(x: leftInset, y: verticalOffset - 3.0), size: trendingTitleSize)
                    verticalOffset += trendingTitleSize.height + 5.0
                }
                commitFileGrid(files: self.files.trending, isTrending: true)
            } else {
                self.trendingTitleNode.isHidden = true
            }
            
            let contentSize = CGSize(width: drawableSize.width, height: verticalOffset + self.bottomInset)
            self.scrollNode.view.contentSize = contentSize
            
            self.displayItems = displayItems
            
            self.validVisibleItemsOffset = nil
            self.currentExtendSizeForTransition = extendSizeForTransition
            self.updateImmediatelyVisibleItems(ensureFrames: true, synchronous: synchronous)
            
            transition.updateAlpha(node: scrollNode, alpha: 1.0, force: true, completion: { [weak self] _ in
                guard let strongSelf = self else {
                    return
                }
                strongSelf.currentExtendSizeForTransition = 0.0
                strongSelf.updateImmediatelyVisibleItems()
            })
        }
    }
    
    @objc func tapGesture(_ recognizer: TapLongTapOrDoubleTapGestureRecognizer) {
        if case .ended = recognizer.state {
            let point = recognizer.location(in: self.view)
            if let (_, file, rect, _) = self.internalFileAt(point: point) {
                if !self.files.isStale {
                    self.fileSelected?(file, self, rect)
                }
            }
        }
    }
    
    public func frameForItem(_ id: MediaId) -> CGRect? {
        for item in self.displayItems {
            if item.file.file.media.fileId == id {
                return item.frame
            }
        }
        return nil
    }
    
    public func fileAt(point: CGPoint) -> (MultiplexedVideoNodeFile, CGRect, Bool)? {
        if let result = self.internalFileAt(point: point) {
            return (result.1, result.2, result.3)
        } else {
            return nil
        }
    }
    
    private func internalFileAt(point: CGPoint) -> (VisibleVideoItem.Id, MultiplexedVideoNodeFile, CGRect, Bool)? {
        let offsetPoint = point.offsetBy(dx: 0.0, dy: self.scrollNode.bounds.minY)
        return self.offsetFileAt(point: offsetPoint)
    }
    
    private func offsetFileAt(point: CGPoint) -> (VisibleVideoItem.Id, MultiplexedVideoNodeFile, CGRect, Bool)? {
        for item in self.displayItems {
            if item.frame.contains(point) {
                let isSaved: Bool
                switch item.id {
                case .saved:
                    isSaved = true
                case .trending:
                    isSaved = false
                }
                return (item.id, item.file, item.frame, isSaved)
            }
        }
        return nil
    }
}

private func NH_LP_TABLE_LOOKUP(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int) -> Int {
    return table[i * rowsize + j]
}

private func NH_LP_TABLE_LOOKUP_SET(_ table: inout [Int], _ i: Int, _ j: Int, _ rowsize: Int, _ value: Int) {
    table[i * rowsize + j] = value
}

private func linearPartitionTable(_ weights: [Int], numberOfPartitions: Int) -> [Int] {
    let n = weights.count
    let k = numberOfPartitions
    
    let tableSize = n * k;
    var tmpTable = Array<Int>(repeatElement(0, count: tableSize))
    
    let solutionSize = (n - 1) * (k - 1)
    var solution = Array<Int>(repeatElement(0, count: solutionSize))
    
    for i in 0 ..< n {
        let offset = i != 0 ? NH_LP_TABLE_LOOKUP(&tmpTable, i - 1, 0, k) : 0
        NH_LP_TABLE_LOOKUP_SET(&tmpTable, i, 0, k, Int(weights[i]) + offset)
    }
    
    for j in 0 ..< k {
        NH_LP_TABLE_LOOKUP_SET(&tmpTable, 0, j, k, Int(weights[0]))
    }
    
    for i in 1 ..< n {
        for j in 1 ..< k {
            var currentMin = 0
            var minX = Int.max
            
            for x in 0 ..< i {
                let c1 = NH_LP_TABLE_LOOKUP(&tmpTable, x, j - 1, k)
                let c2 = NH_LP_TABLE_LOOKUP(&tmpTable, i, 0, k) - NH_LP_TABLE_LOOKUP(&tmpTable, x, 0, k)
                let cost = max(c1, c2)
                
                if x == 0 || cost < currentMin {
                    currentMin = cost;
                    minX = x
                }
            }
            
            NH_LP_TABLE_LOOKUP_SET(&tmpTable, i, j, k, currentMin)
            NH_LP_TABLE_LOOKUP_SET(&solution, i - 1, j - 1, k - 1, minX)
        }
    }
    
    return solution
}

private func linearPartitionForWeights(_ weights: [Int], numberOfPartitions: Int) -> [[Int]] {
    var n = weights.count
    var k = numberOfPartitions
    
    if k <= 0 {
        return []
    }
    
    if k >= n {
        var partition: [[Int]] = []
        for weight in weights {
            partition.append([weight])
        }
        return partition
    }
    
    if n == 1 {
        return [weights]
    }
    
    var solution = linearPartitionTable(weights, numberOfPartitions: numberOfPartitions)
    let solutionRowSize = numberOfPartitions - 1
    
    k = k - 2;
    n = n - 1;
    
    var answer: [[Int]] = []
    
    while k >= 0 {
        if n < 1 {
            answer.insert([], at: 0)
        } else {
            var currentAnswer: [Int] = []
            
            var i = NH_LP_TABLE_LOOKUP(&solution, n - 1, k, solutionRowSize) + 1
            let range = n + 1
            while i < range {
                currentAnswer.append(weights[i])
                i += 1
            }
            
            answer.insert(currentAnswer, at: 0)
            
            n = NH_LP_TABLE_LOOKUP(&solution, n - 1, k, solutionRowSize)
        }
        
        k = k - 1
    }
    
    var currentAnswer: [Int] = []
    var i = 0
    let range = n + 1
    while i < range {
        currentAnswer.append(weights[i])
        i += 1
    }
    
    answer.insert(currentAnswer, at: 0)
    
    return answer
}