import Adamantium
import AnalyticsClient
import APIClient
import AVFoundation
import AVKit
import CommentsClient
import ComponentLibrary
import ComposableArchitecture
import EventBusClient
import FeatureClipDetail
import FeatureShare
import FeatureShareSheet
import FeatureToasts
import Localization
import PlayerClient
import StatsigClient
import SwiftUI
import Utilities

class MultiGestureContainerView: UIView, UIGestureRecognizerDelegate {
    private enum Constants {
        // Space reserved at bottom of screen
        static let headerBottomPadding: CGFloat = 150
        // Height of the header itself
        static let headerHeight: CGFloat = 70
        // Page change animation duration
        static let pageChangeAnimationDuration: TimeInterval = 0.3
        // Drag percentage threshold
        static let dragPercentageThreshold: CGFloat = 0.15
        // Quick flick threshold
        static let quickFlickThreshold: CGFloat = 200
        // Playback debounce delay
        static let playbackDebounceDelay: CGFloat = 50
    }

    let collectionView: UICollectionView
    let scrollView: VerticalClipScrollView
    private var horizontalPanRecognizer: UIPanGestureRecognizer?
    private var gradientOverlayView: UIView?
    private var gradientLayer: CAGradientLayer?
    private var blurOverlayView: UIVisualEffectView?

    private var hasPerformedInitialScroll = false

    var onCollectionViewPageChange: ((Int) -> Void)?
    var lastKnownIndex: Int = 0
    private var pageChangeTask: Task<Void, Never>?

    var clips: [Clip] = []
    private var currentClipIDs: [Clip.ID] = []
    private var activeSnapshotView: UIView?
    private var isProgrammaticIndexUpdate = false
    private var isGestureUpdate = false
    private var previousSelectedIndex: Int = 0
    var selectedIndex: Int = 0 {
        didSet {
            guard !isProgrammaticIndexUpdate, !isGestureUpdate else { return }
            updateSelectedIndex(previousIndex: oldValue)
        }
    }

    var verticalScrollPosition: CGFloat = 0
    var isScrubbing: Bool = false
    var instantBlur: Bool = false {
        didSet {
            updateBlurEffect()
        }
    }

    var dimGradient: Bool = false {
        didSet {
            updateGradientOpacity()
        }
    }

    var playClipAtHorizontalIndex: ((Int) -> Void)?

    var verticalContentScrollOffset: ((CGFloat) -> Void)?

    // Callbacks for infinite carousel queue rotation
    var onRotateQueueForward: (() async -> Void)?
    var onRotateQueueBackward: (() async -> Void)?

    // Track if gesture started outside playbar to handle edge case
    private var gestureStartedOutsidePlaybar: Bool = false

    init(scrollView: VerticalClipScrollView) {
        // Create the collection view first
        let layout = UICollectionViewFlowLayout()
        layout.scrollDirection = .horizontal
        layout.minimumLineSpacing = 0
        layout.minimumInteritemSpacing = 0

        // Use exact screen size for cells
        let screenSize = UIScreen.main.bounds.size
        layout.itemSize = screenSize

        let cv = UICollectionView(frame: .zero, collectionViewLayout: layout)
        cv.isPagingEnabled = true
        cv.showsHorizontalScrollIndicator = false
        cv.backgroundColor = .clear
        cv.register(ClipBackgroundMediaCell.self, forCellWithReuseIdentifier: "ClipBackgroundMediaCell")
        cv.contentInsetAdjustmentBehavior = .never
        cv.clipsToBounds = false // Allow content to extend beyond bounds

        self.collectionView = cv
        self.scrollView = scrollView

        super.init(frame: .zero)

        // Set the container view's background to clear
        backgroundColor = .clear

        // Configure collection view
        collectionView.dataSource = self
        collectionView.delegate = self
        collectionView.delaysContentTouches = false
        collectionView.canCancelContentTouches = true

        // Add background layer (clip song art)
        addSubview(collectionView)

        // Create and add blur overlay view (below gradient, above collection view)
        let blurEffect = UIBlurEffect(style: .dark)
        let blurEffectView = UIVisualEffectView(effect: nil) // Start with no effect
        blurEffectView.isUserInteractionEnabled = false
        addSubview(blurEffectView)
        self.blurOverlayView = blurEffectView

        // Create and add gradient overlay (top layer)
        let gradientView = UIView()
        gradientView.backgroundColor = .clear
        gradientView.isUserInteractionEnabled = false // Don't block touch events
        addSubview(gradientView)
        self.gradientOverlayView = gradientView

        // Add content layer (clip title, artist, caption, lyrics, etc.)
        addSubview(scrollView)

        // Create and configure gradient layer
        let gradient = CAGradientLayer()
        gradient.colors = [
            UIColor.black.withAlphaComponent(0.2).cgColor,
            UIColor.black.withAlphaComponent(0.0).cgColor,
            UIColor.black.withAlphaComponent(0.0).cgColor,
            UIColor.black.withAlphaComponent(0.4).cgColor,
            UIColor.black.withAlphaComponent(0.6).cgColor,
        ]
        gradient.locations = [0.0, 0.15, 0.35, 0.6, 1.0]
        gradient.startPoint = CGPoint(x: 0.5, y: 0.0)
        gradient.endPoint = CGPoint(x: 0.5, y: 1.0)
        gradientView.layer.addSublayer(gradient)
        self.gradientLayer = gradient

        // Set up constraints
        scrollView.translatesAutoresizingMaskIntoConstraints = false
        collectionView.translatesAutoresizingMaskIntoConstraints = false
        gradientView.translatesAutoresizingMaskIntoConstraints = false
        blurOverlayView?.translatesAutoresizingMaskIntoConstraints = false

        NSLayoutConstraint.activate([
            collectionView.topAnchor.constraint(equalTo: topAnchor),
            collectionView.leadingAnchor.constraint(equalTo: leadingAnchor),
            collectionView.trailingAnchor.constraint(equalTo: trailingAnchor),
            collectionView.bottomAnchor.constraint(equalTo: bottomAnchor),

            blurOverlayView!.topAnchor.constraint(equalTo: topAnchor),
            blurOverlayView!.leadingAnchor.constraint(equalTo: leadingAnchor),
            blurOverlayView!.trailingAnchor.constraint(equalTo: trailingAnchor),
            blurOverlayView!.bottomAnchor.constraint(equalTo: bottomAnchor),

            scrollView.topAnchor.constraint(equalTo: topAnchor),
            scrollView.leadingAnchor.constraint(equalTo: leadingAnchor),
            scrollView.trailingAnchor.constraint(equalTo: trailingAnchor),
            scrollView.bottomAnchor.constraint(equalTo: bottomAnchor),

            gradientView.topAnchor.constraint(equalTo: topAnchor),
            gradientView.leadingAnchor.constraint(equalTo: leadingAnchor),
            gradientView.trailingAnchor.constraint(equalTo: trailingAnchor),
            gradientView.bottomAnchor.constraint(equalTo: bottomAnchor),
        ])

        // Make scroll view's background clear so collection view is visible underneath
        scrollView.backgroundColor = .clear

        // Horizontal gestures on the scroll view should fail if needed
        // and be handled by our gesture recognizer below
        let horizontalPan = UIPanGestureRecognizer(target: self, action: #selector(handleHorizontalPan(_:)))
        horizontalPan.delegate = self
        horizontalPan.cancelsTouchesInView = true
        self.horizontalPanRecognizer = horizontalPan
        addGestureRecognizer(horizontalPan)
        scrollView.panGestureRecognizer.require(toFail: horizontalPan)

        scrollView.verticalContentScrollOffset = { [weak self] offset in
            guard let self else { return }
            self.verticalScrollPosition = max(0, offset)
            self.verticalContentScrollOffset?(offset)

            // Update blur effect based on current state
            self.updateBlurEffect()
        }

        scrollView.resetScrollPosition()
    }

    @available(*, unavailable)
    required init?(coder _: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    override func layoutSubviews() {
        super.layoutSubviews()

        // Update gradient layer frame without implicit animations
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        gradientLayer?.frame = bounds
        CATransaction.commit()

        // Perform initial scroll after layout is complete
        if !hasPerformedInitialScroll, selectedIndex < clips.count {
            hasPerformedInitialScroll = true
            lastKnownIndex = selectedIndex
            collectionView.scrollToItem(
                at: IndexPath(row: selectedIndex, section: 0),
                at: .centeredHorizontally,
                animated: false
            )
        }
    }

    func updateSelectedIndex(previousIndex _: Int) {
        if selectedIndex < clips.count {
            lastKnownIndex = selectedIndex

            let indexPath = IndexPath(row: selectedIndex, section: 0)
            guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
            let pageWidth = layout.itemSize.width
            let targetOffset = CGPoint(x: CGFloat(selectedIndex) * pageWidth, y: 0)

            // Cancel any ongoing animations to prevent interrupted completion blocks
            collectionView.layer.removeAllAnimations()

            // Always clean up existing snapshot and create fresh one to prevent accumulation
            aggressiveSnapshotCleanup()
            activeSnapshotView = createTransitionSnapshot()

            UIView.animate(withDuration: Constants.pageChangeAnimationDuration, delay: 0, options: [.curveEaseInOut], animations: {
                self.collectionView.setContentOffset(targetOffset, animated: false)
            }, completion: { finished in
                self.collectionView.layoutIfNeeded()
                self.scrollView.resetScrollPosition()

                // Only clean up if animation actually finished (not interrupted)
                if finished {
                    self.cleanupSnapshot()
                }
            })
        }
    }

    @objc private func handleHorizontalPan(_ gesture: UIPanGestureRecognizer) {
        // Handle cancellation and failure explicitly
        if gesture.state == .cancelled || gesture.state == .failed {
            cleanupSnapshot()
            return
        }

        guard !isScrubbing else {
            return
        }

        let velocity = gesture.velocity(in: self)
        // Check if the gesture is in the playbar area
        let location = gesture.location(in: self)
        let touchInPlaybar = isInPlaybarArea(location)

        if gesture.state == .began {
            cleanupSnapshot()
            activeSnapshotView = createTransitionSnapshot()
            gestureStartedOutsidePlaybar = !touchInPlaybar
        }

        // Don't handle horizontal gestures in the playbar area
        if !touchInPlaybar {
            if gesture.state == .began || gesture.state == .changed {
                let location = gesture.location(in: collectionView)
                let translation = gesture.translation(in: collectionView)

                // Apply translation directly to collection view's content offset
                var newOffset = collectionView.contentOffset
                newOffset.x -= translation.x

                // Ensure offset is within bounds
                let maxOffsetX = collectionView.contentSize.width - collectionView.bounds.width
                newOffset.x = max(0, min(maxOffsetX, newOffset.x))

                collectionView.setContentOffset(newOffset, animated: false)
                gesture.setTranslation(.zero, in: collectionView)

            } else if gesture.state == .ended {
                // When the gesture ends, determine page change based on drag distance and direction
                let gestureVelocity = abs(velocity.x)
                let pageWidth = collectionView.bounds.width

                // Use the starting position instead of current dragged position to avoid double-jumping
                let startingPage = lastKnownIndex

                // Calculate how far we've dragged from the starting position
                let currentOffset = collectionView.contentOffset.x
                let startingOffset = CGFloat(startingPage) * pageWidth
                let dragDistance = currentOffset - startingOffset
                let dragPercentage = abs(dragDistance) / pageWidth
                // Determine target page based on drag direction:
                // Dragging left (negative offset) = previous clip
                // Dragging right (positive offset) = next clip
                let targetPage = dragDistance < 0 ? startingPage - 1 : startingPage + 1

                // Change pages if both conditions are met:
                // 1. Drag distance is over threshold OR velocity is high enough for a quick flick
                // 2. Target page is valid and different from starting page
                // 3. Velocity direction matches drag direction (or is minimal)
                let didDragEnough = dragPercentage > Constants.dragPercentageThreshold

                // For quick flicks, don't require dragDistance - except at edges to prevent bouncing in the other direction
                let isAtLeftEdge = startingPage == 0 && velocity.x > 0 // At first clip, swiping right
                let isAtRightEdge = startingPage == clips.count - 1 && velocity.x < 0 // At last clip, swiping left
                let shouldRequireDragAtEdge = isAtLeftEdge || isAtRightEdge

                let didDragFastEnough = gestureVelocity > Constants.quickFlickThreshold && (!shouldRequireDragAtEdge || dragDistance != 0)
                let targetPageIsValid = targetPage >= 0 && targetPage < clips.count
                let targetPageChanged = targetPage != startingPage

                // Check if velocity direction matches drag direction
                // Note: velocity is inverted from drag direction due to coordinate systems
                let velocityMatchesDragDirection: Bool
                if dragDistance == 0 {
                    velocityMatchesDragDirection = false
                } else if dragDistance < 0 {
                    // Dragging left (to previous clip), velocity should be right (positive)
                    velocityMatchesDragDirection = velocity.x > 0
                } else {
                    // Dragging right (to next clip), velocity should be left (negative)
                    velocityMatchesDragDirection = velocity.x < 0
                }
                if didDragEnough || didDragFastEnough,
                   targetPageIsValid,
                   targetPageChanged,
                   velocityMatchesDragDirection || (dragDistance == 0 && didDragFastEnough)
                {
                    // For quick flicks with no drag tracking, determine direction from velocity
                    let finalTargetPage: Int
                    if dragDistance == 0, didDragFastEnough {
                        if velocity.x < 0 {
                            // Fast swipe left = next clip
                            finalTargetPage = min(startingPage + 1, clips.count - 1)
                        } else {
                            // Fast swipe right = previous clip
                            finalTargetPage = max(startingPage - 1, 0)
                        }
                    } else {
                        finalTargetPage = targetPage
                    }

                    beginPageChangeFromSwipe(finalTargetPage, gestureVelocity: gestureVelocity)
                } else {
                    // Snap back to original position with smooth animation
                    snapBackToPage(startingPage)
                    cleanupSnapshot()
                }
            }
        } else {
            // Handle the edge case: gesture started outside playbar but ended inside
            if gesture.state == .ended, gestureStartedOutsidePlaybar {
                // Clean up properly - snap back to original position
                snapBackToPage(lastKnownIndex)
                cleanupSnapshot()
            } else {
                // Clean up snapshot if gesture started in playbar area
                cleanupSnapshot()
            }
        }
    }

    private func beginPageChangeFromSwipe(_ page: Int, gestureVelocity: CGFloat = 0) {
        pageChangeTask?.cancel()

        lastKnownIndex = page

        let indexPath = IndexPath(row: page, section: 0)
        guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        let pageWidth = layout.itemSize.width
        let targetOffset = CGPoint(x: CGFloat(page) * pageWidth, y: 0)

        // Use faster animation for quick flicks, slower for regular swipes
        var animationDuration: TimeInterval = Constants.pageChangeAnimationDuration
        if gestureVelocity > Constants.quickFlickThreshold {
            // Scale if over quick flick threshold
            let scaleFactor = min(max(abs(gestureVelocity) / 300, 2.0), 1.0)
            animationDuration = Constants.pageChangeAnimationDuration / scaleFactor
        }

        // Cancel ongoing animations but keep the current snapshot for transition
        collectionView.layer.removeAllAnimations()

        UIView.animate(withDuration: animationDuration, delay: 0, options: [.curveEaseOut], animations: {
            self.collectionView.setContentOffset(targetOffset, animated: false)
        }, completion: { finished in
            self.collectionView.layoutIfNeeded()
            self.scrollView.resetScrollPosition()

            if finished {
                self.aggressiveSnapshotCleanup()
            }
        })

        isGestureUpdate = true
        selectedIndex = page
        isGestureUpdate = false

        // Handle infinite carousel and debounced playback
        handlePageChange(for: page, gestureVelocity: gestureVelocity)
    }

    private func beginPageChangeFromPlaybackControls(_ page: Int) {
        pageChangeTask?.cancel()

        lastKnownIndex = page

        let indexPath = IndexPath(row: page, section: 0)
        guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        let pageWidth = layout.itemSize.width
        let targetOffset = CGPoint(x: CGFloat(page) * pageWidth, y: 0)

        // Cancel any ongoing animations and clean up for programmatic changes
        collectionView.layer.removeAllAnimations()
        aggressiveSnapshotCleanup()
        activeSnapshotView = createTransitionSnapshot()

        UIView.animate(withDuration: Constants.pageChangeAnimationDuration, delay: 0, options: [.curveEaseOut], animations: {
            self.collectionView.setContentOffset(targetOffset, animated: false)
        }, completion: { finished in
            self.collectionView.layoutIfNeeded()
            self.scrollView.resetScrollPosition()

            if finished {
                self.cleanupSnapshot()
            }
        })

        // Handle infinite carousel and debounced playback (no gesture velocity for programmatic changes)
        handlePageChange(for: page)
    }

    private func snapBackToPage(_ page: Int) {
        guard let layout = collectionView.collectionViewLayout as? UICollectionViewFlowLayout else { return }
        let pageWidth = layout.itemSize.width
        let targetOffset = CGPoint(x: CGFloat(page) * pageWidth, y: 0)

        // Cancel ongoing animations but keep current snapshot for transition
        collectionView.layer.removeAllAnimations()

        UIView.animate(withDuration: Constants.pageChangeAnimationDuration, delay: 0, options: [.curveEaseOut], animations: {
            self.collectionView.setContentOffset(targetOffset, animated: false)
        }, completion: { finished in
            self.collectionView.layoutIfNeeded()
            self.scrollView.resetScrollPosition()

            if finished {
                self.aggressiveSnapshotCleanup()
            }
        })
    }

    // Helper function to handle page update, infinite carousel rotation, and debounced playback
    private func handlePageChange(for page: Int, gestureVelocity: CGFloat = 0) {
        self.onCollectionViewPageChange?(page)

        // Rotate queue when approaching edges
        Task {
            if page >= clips.count - 2 {
                await onRotateQueueForward?()
            } else if page <= 1 {
                await onRotateQueueBackward?()
            }
        }

        // Debounce to prevent rapid firing during deceleration
        let debounceDelay = gestureVelocity > Constants.quickFlickThreshold ? Constants.playbackDebounceDelay * 0.8 : Constants.playbackDebounceDelay
        pageChangeTask = Task { @MainActor in
            try? await Task.sleep(for: .milliseconds(debounceDelay))
            guard !Task.isCancelled else { return }
            playClipAtHorizontalIndex?(page)
        }
    }

    // Add a method to check if the gesture is in the playbar area
    private func isInPlaybarArea(_ location: CGPoint) -> Bool {
        // Playbar is in the bottom ~30% of the screen
        let playbarYThreshold = bounds.height * 0.72
        let isInPlaybar = location.y > playbarYThreshold
        return isInPlaybar
    }

    override func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
        // Use the vertical scroll view offset to determine if we should handle the gesture
        if gestureRecognizer == horizontalPanRecognizer, let pan = gestureRecognizer as? UIPanGestureRecognizer {
            let location = pan.location(in: self)

            let velocity = pan.velocity(in: self)
            // Only handle predominantly horizontal gestures
            let isHorizontal = abs(velocity.x) > abs(velocity.y)

            // If it's a vertical drag, always reject so it can go to OmniPlayerView
            if !isHorizontal {
                return false
            }

            if isInPlaybarArea(location) || isScrubbing {
                return false
            }

            let isInHorizontalSwipingRegion = location.y < UIScreen.height * 0.72
            let scrollThreshold = UIScreen.height * 0.4
            let hasScrolledDownPastThreshold = verticalScrollPosition > scrollThreshold

            let shouldBegin = isHorizontal && isInHorizontalSwipingRegion && !hasScrolledDownPastThreshold
            return shouldBegin
        }
        return true
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldReceive touch: UITouch) -> Bool {
        // Only block touches for horizontal pan recognizer if they would interfere with progress bar scrubbing
        if gestureRecognizer == horizontalPanRecognizer {
            let location = touch.location(in: self)
            let inPlaybar = isInPlaybarArea(location)

            // Only block if this might be a horizontal drag in the progress bar area or scrubbing
            // Allow all vertical drags to pass through for player collapse functionality
            if inPlaybar || isScrubbing {
                return true // Allow the touch, let gestureRecognizerShouldBegin decide
            }
        }
        return true
    }

    func gestureRecognizer(_ gestureRecognizer: UIGestureRecognizer, shouldRecognizeSimultaneouslyWith otherGestureRecognizer: UIGestureRecognizer) -> Bool {
        // Check for horizontal pan in playbar area
        if gestureRecognizer == horizontalPanRecognizer, let pan = gestureRecognizer as? UIPanGestureRecognizer {
            let location = pan.location(in: self)

            // Never interfere with playbar area drags (includes progress bar)
            if isInPlaybarArea(location) {
                return false
            }

            // If this is a horizontal gesture, don't let other gestures interfere
            let velocity = pan.velocity(in: self)
            let isHorizontal = abs(velocity.x) > abs(velocity.y)
            if isHorizontal {
                return false
            }
        }

        // Don't recognize scroll view's pan gesture simultaneously with our horizontal pan
        if gestureRecognizer == horizontalPanRecognizer && otherGestureRecognizer == scrollView.panGestureRecognizer {
            return false
        }

        // For other gestures, we can be more flexible
        return true
    }

    func scrollDownToShowClipDetails() {
        // Offset by 220 to account for top safe area and the new artist details card
        let currentOffset = scrollView.contentOffset.y
        let newOffset = CGPoint(x: scrollView.contentOffset.x, y: currentOffset + UIScreen.height - 220)

        scrollView.setContentOffset(newOffset, animated: true)
    }

    func scrollToTop() {
        scrollView.resetScrollPosition(animated: true)
    }

    private func updateBlurEffect() {
        let maxScrollOffset: CGFloat = UIScreen.main.bounds.height - 500
        let scrollBlurProgress = min(1.0, verticalScrollPosition / maxScrollOffset)

        // Use instant blur if enabled, otherwise use scroll-based blur
        let blurEffect = UIBlurEffect(style: .dark)
        if instantBlur {
            blurOverlayView?.effect = blurEffect
            blurOverlayView?.alpha = 0.8 // Keep a slight blur for instant blur mode
        } else if scrollBlurProgress > 0 {
            blurOverlayView?.effect = blurEffect
            blurOverlayView?.alpha = scrollBlurProgress
        } else {
            blurOverlayView?.effect = nil
            blurOverlayView?.alpha = 0
        }
    }

    private func updateGradientOpacity() {
        CATransaction.begin()
        CATransaction.setAnimationDuration(0.3)
        CATransaction.setAnimationTimingFunction(CAMediaTimingFunction(name: .easeInEaseOut))

        if dimGradient {
            gradientLayer?.colors = [
                UIColor.black.withAlphaComponent(0.1).cgColor,
                UIColor.black.withAlphaComponent(0.0).cgColor,
                UIColor.black.withAlphaComponent(0.0).cgColor,
                UIColor.black.withAlphaComponent(0.2).cgColor,
                UIColor.black.withAlphaComponent(0.4).cgColor,
            ]
        } else {
            gradientLayer?.colors = [
                UIColor.black.withAlphaComponent(0.2).cgColor,
                UIColor.black.withAlphaComponent(0.0).cgColor,
                UIColor.black.withAlphaComponent(0.0).cgColor,
                UIColor.black.withAlphaComponent(0.4).cgColor,
                UIColor.black.withAlphaComponent(0.6).cgColor,
            ]
        }

        CATransaction.commit()
    }

    private func createTransitionSnapshot() -> UIView? {
        guard let currentCell = collectionView.visibleCells.first else {
            return nil
        }

        let snapshot = currentCell.snapshotView(afterScreenUpdates: false)
        guard let snapshot else {
            return nil
        }

        snapshot.frame = currentCell.frame
        collectionView.addSubview(snapshot)

        return snapshot
    }

    // Clean up the snapshot view as we swipe to the next clip
    private func cleanupSnapshot() {
        activeSnapshotView?.removeFromSuperview()
        activeSnapshotView = nil
    }

    // Remove all snapshot views that might be left behind
    private func aggressiveSnapshotCleanup() {
        for subview in collectionView.subviews {
            // Check if it's a snapshot view
            if subview is UIView, !(subview is UICollectionViewCell) {
                subview.removeFromSuperview()
            }
        }
        activeSnapshotView = nil
    }
}

// MARK: - UICollectionViewDataSource & UICollectionViewDelegate

extension MultiGestureContainerView: UICollectionViewDataSource, UICollectionViewDelegate {
    func collectionView(_: UICollectionView, numberOfItemsInSection _: Int) -> Int {
        return clips.count
    }

    func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        let cell = collectionView.dequeueReusableCell(withReuseIdentifier: "ClipBackgroundMediaCell", for: indexPath) as! ClipBackgroundMediaCell
        let clip = clips[indexPath.row]
        cell.configure(
            clip: clip,
            hasPlayableScene: clip.videoToSongVideoOutputUrl != nil,
            hasPlayableInputVideo: false
        )
        return cell
    }

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        if scrollView == collectionView {}
    }

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        if scrollView == collectionView {
            let pageWidth = scrollView.bounds.width
            let page = Int(round(scrollView.contentOffset.x / pageWidth))

            if page != lastKnownIndex, page < clips.count {
                beginPageChangeFromPlaybackControls(page)
            }
        }
    }
}
