import UIKit

public struct LNIndexSection: Equatable {
    public var index: Int
    public var section: Int

    public init(index: Int, section: Int) {
        self.index = index
        self.section = section
    }
}

public enum LNPagerScrollDirection {
    case left
    case right
}

public final class LNCyclePager: UIView {
    private let maxSectionCount = 200
    private let minSectionCount = 18

    private let transformLayout = LNCyclePagerTransformLayout()
    private(set) lazy var collectionView: UICollectionView = {
        let view = UICollectionView(frame: .zero, collectionViewLayout: transformLayout)
        view.backgroundColor = .clear
        view.showsHorizontalScrollIndicator = false
        view.showsVerticalScrollIndicator = false
        view.dataSource = self
        view.delegate = self
        view.decelerationRate = UIScrollView.DecelerationRate(rawValue: 1 - 0.0076)
        if #available(iOS 10.0, *) {
            view.isPrefetchingEnabled = false
        }
        return view
    }()

    public var numberOfItemsProvider: (() -> Int)?
    public var cellProvider: ((_ pagerView: LNCyclePager, _ index: Int) -> UICollectionViewCell)?
    public var layoutProvider: (() -> LNCyclePagerViewLayout)?

    public var didScrollFromIndexToIndex: ((_ from: Int, _ to: Int) -> Void)?
    public var didSelectItem: ((_ cell: UICollectionViewCell, _ index: Int) -> Void)?
    public var didSelectItemAtIndexSection: ((_ cell: UICollectionViewCell, _ indexSection: LNIndexSection) -> Void)?
    public var didScroll: (() -> Void)?

    public var initializeTransformAttributes: ((UICollectionViewLayoutAttributes) -> Void)? {
        didSet { updateTransformLayoutDelegateBinding() }
    }
    public var applyTransformAttributes: ((UICollectionViewLayoutAttributes) -> Void)? {
        didSet { updateTransformLayoutDelegateBinding() }
    }

    public var backgroundView: UIView? {
        get { collectionView.backgroundView }
        set { collectionView.backgroundView = newValue }
    }

    public var layout: LNCyclePagerViewLayout? {
        didSet {
            layout?.isInfiniteLoop = isInfiniteLoop
            updateTransformLayoutDelegateBinding()
        }
    }

    public var isInfiniteLoop = true {
        didSet {
            layout?.isInfiniteLoop = isInfiniteLoop
            reloadData()
        }
    }

    public var autoScrollInterval: TimeInterval = 0 {
        didSet {
            stopTimer()
            if autoScrollInterval > 0, superview != nil {
                startTimerIfNeeded()
            }
        }
    }

    public var reloadDataNeedResetIndex = false
    public var currentIndex: Int { indexSection.index }
    public private(set) var indexSection = LNIndexSection(index: -1, section: -1)

    public var contentOffset: CGPoint { collectionView.contentOffset }
    public var tracking: Bool { collectionView.isTracking }
    public var dragging: Bool { collectionView.isDragging }
    public var decelerating: Bool { collectionView.isDecelerating }

    private var timer: Timer?
    private var numberOfItems = 0
    private var dequeueSection = 0
    private var beginDragIndexSection = LNIndexSection(index: 0, section: 0)
    private var firstScrollIndex = -1

    private var needClearLayout = false
    private var didReloadData = false
    private var didLayout = false
    private var needResetIndex = false
    private weak var boundPageControl: LNCyclePageControl?

    public override init(frame: CGRect) {
        super.init(frame: frame)
        addSubview(collectionView)
        updateTransformLayoutDelegateBinding()
    }

    public required init?(coder: NSCoder) {
        super.init(coder: coder)
        addSubview(collectionView)
        updateTransformLayoutDelegateBinding()
    }

    deinit {
        stopTimer()
    }

    public override func didMoveToSuperview() {
        super.didMoveToSuperview()
        stopTimer()
        if superview != nil, autoScrollInterval > 0 {
            startTimerIfNeeded()
        }
    }

    public override func layoutSubviews() {
        super.layoutSubviews()
        let needUpdateLayout = collectionView.frame != bounds
        collectionView.frame = bounds
        if (indexSection.section < 0 || needUpdateLayout), (numberOfItems > 0 || didReloadData) {
            didLayout = true
            setNeedUpdateLayout()
        }
    }

    public func bindPageControl(_ pageControl: LNCyclePageControl?) {
        boundPageControl = pageControl
        guard let pageControl else { return }
        pageControl.numberOfPages = numberOfItems
        pageControl.currentPage = max(indexSection.index, 0)
    }

    public func register(_ cellClass: AnyClass, forCellWithReuseIdentifier identifier: String) {
        collectionView.register(cellClass, forCellWithReuseIdentifier: identifier)
    }

    public func register(_ nib: UINib, forCellWithReuseIdentifier identifier: String) {
        collectionView.register(nib, forCellWithReuseIdentifier: identifier)
    }

    public func dequeueReusableCell(withReuseIdentifier identifier: String, for index: Int) -> UICollectionViewCell {
        collectionView.dequeueReusableCell(withReuseIdentifier: identifier, for: IndexPath(item: index, section: dequeueSection))
    }

    public func reloadData() {
        didReloadData = true
        needResetIndex = true
        setNeedClearLayout()
        clearLayoutIfNeeded()
        updateData()
    }

    public func updateData() {
        updateLayout()
        numberOfItems = max(0, numberOfItemsProvider?() ?? 0)
        collectionView.reloadData()

        if !didLayout, !collectionView.frame.isEmpty, indexSection.index < 0 {
            didLayout = true
        }

        let shouldReset = needResetIndex && reloadDataNeedResetIndex
        needResetIndex = false

        if shouldReset {
            stopTimer()
        }

        let fallback = (indexSection.index < 0 && !collectionView.frame.isEmpty) || shouldReset ? 0 : indexSection.index
        resetPagerView(at: fallback)

        boundPageControl?.numberOfPages = numberOfItems
        boundPageControl?.setCurrentPage(max(indexSection.index, 0), animate: false)

        if shouldReset {
            startTimerIfNeeded()
        }
    }

    public func setNeedUpdateLayout() {
        guard effectiveLayout() != nil else { return }
        clearLayoutIfNeeded()
        updateLayout()
        collectionView.collectionViewLayout.invalidateLayout()
        resetPagerView(at: indexSection.index < 0 ? 0 : indexSection.index)
    }

    public func setNeedClearLayout() {
        needClearLayout = true
    }

    public func currentIndexCell() -> UICollectionViewCell? {
        guard indexSection.index >= 0, indexSection.section >= 0 else { return nil }
        return collectionView.cellForItem(at: IndexPath(item: indexSection.index, section: indexSection.section))
    }

    public func visibleCells() -> [UICollectionViewCell] {
        collectionView.visibleCells
    }

    public func visibleIndexes() -> [Int] {
        collectionView.indexPathsForVisibleItems.map(\.item)
    }

    public func scrollToItem(at index: Int, animated: Bool) {
        if !didLayout, didReloadData {
            firstScrollIndex = index
        } else {
            firstScrollIndex = -1
        }

        if !isInfiniteLoop {
            scrollToItem(at: LNIndexSection(index: index, section: 0), animated: animated)
            return
        }

        let targetSection = index >= currentIndex ? indexSection.section : indexSection.section + 1
        scrollToItem(at: LNIndexSection(index: index, section: targetSection), animated: animated)
    }

    public func scrollToItem(at target: LNIndexSection, animated: Bool) {
        guard numberOfItems > 0, isValid(target) else { return }
        let offset = calculateOffset(at: target)
        if resolvedScrollDirection() == .horizontal {
            collectionView.setContentOffset(CGPoint(x: offset, y: collectionView.contentOffset.y), animated: animated)
        } else {
            collectionView.setContentOffset(CGPoint(x: collectionView.contentOffset.x, y: offset), animated: animated)
        }
    }

    public func scrollToNearlyIndex(at direction: LNPagerScrollDirection, animated: Bool) {
        let target = nearlyIndexSection(from: indexSection, direction: direction)
        scrollToItem(at: target, animated: animated)
    }

    private func startTimerIfNeeded() {
        guard timer == nil, autoScrollInterval > 0 else { return }
        let timer = Timer(timeInterval: autoScrollInterval, repeats: true) { [weak self] _ in
            self?.timerFired()
        }
        RunLoop.main.add(timer, forMode: .common)
        self.timer = timer
    }

    private func stopTimer() {
        timer?.invalidate()
        timer = nil
    }

    private func timerFired() {
        guard superview != nil, window != nil, numberOfItems > 0, !tracking else { return }
        scrollToNearlyIndex(at: .right, animated: true)
    }

    private func updateTransformLayoutDelegateBinding() {
        transformLayout.transformDelegate = (applyTransformAttributes != nil || initializeTransformAttributes != nil) ? self : nil
    }

    private func effectiveLayout() -> LNCyclePagerViewLayout? {
        if layout == nil, let provider = layoutProvider {
            layout = provider()
            layout?.isInfiniteLoop = isInfiniteLoop
        }
        if let layout, layout.itemSize.width > 0, layout.itemSize.height > 0 {
            return layout
        }
        layout = nil
        return nil
    }

    private func updateLayout() {
        guard let layout = effectiveLayout() else { return }
        layout.isInfiniteLoop = isInfiniteLoop
        transformLayout.layoutConfig = layout
    }

    private func clearLayoutIfNeeded() {
        if needClearLayout {
            layout = nil
            transformLayout.layoutConfig = nil
            needClearLayout = false
        }
    }

    private func isValid(_ target: LNIndexSection) -> Bool {
        target.index >= 0 &&
        target.index < numberOfItems &&
        target.section >= 0 &&
        target.section < maxSectionCount
    }

    private func nearlyIndexSection(from current: LNIndexSection, direction: LNPagerScrollDirection) -> LNIndexSection {
        guard current.index >= 0, current.index < numberOfItems else { return current }

        if !isInfiniteLoop {
            if direction == .right, current.index == numberOfItems - 1 {
                return autoScrollInterval > 0 ? LNIndexSection(index: 0, section: 0) : current
            } else if direction == .right {
                return LNIndexSection(index: current.index + 1, section: 0)
            }

            if current.index == 0 {
                return autoScrollInterval > 0 ? LNIndexSection(index: numberOfItems - 1, section: 0) : current
            }
            return LNIndexSection(index: current.index - 1, section: 0)
        }

        if direction == .right {
            if current.index < numberOfItems - 1 {
                return LNIndexSection(index: current.index + 1, section: current.section)
            }
            if current.section >= maxSectionCount - 1 {
                return LNIndexSection(index: current.index, section: maxSectionCount - 1)
            }
            return LNIndexSection(index: 0, section: current.section + 1)
        }

        if current.index > 0 {
            return LNIndexSection(index: current.index - 1, section: current.section)
        }
        if current.section <= 0 {
            return LNIndexSection(index: current.index, section: 0)
        }
        return LNIndexSection(index: numberOfItems - 1, section: current.section - 1)
    }

    private func calculateIndexSection(with offset: CGPoint) -> LNIndexSection {
        guard numberOfItems > 0 else { return LNIndexSection(index: 0, section: 0) }
        guard let layout = effectiveLayout() else { return LNIndexSection(index: 0, section: 0) }

        let itemLength = (resolvedScrollDirection() == .horizontal ? transformLayout.itemSize.width : transformLayout.itemSize.height) + transformLayout.minimumInteritemSpacing
        guard itemLength > 0 else { return LNIndexSection(index: 0, section: 0) }

        let edge = isInfiniteLoop
            ? (resolvedScrollDirection() == .horizontal ? layout.sectionInset.left : layout.sectionInset.top)
            : (resolvedScrollDirection() == .horizontal ? layout.onlyOneSectionInset.left : layout.onlyOneSectionInset.top)

        let viewportLength = resolvedScrollDirection() == .horizontal ? collectionView.bounds.width : collectionView.bounds.height
        let middleOffset = (resolvedScrollDirection() == .horizontal ? offset.x : offset.y) + viewportLength * 0.5

        guard middleOffset - edge >= 0 else { return LNIndexSection(index: 0, section: 0) }
        let raw = Int((middleOffset - edge + transformLayout.minimumInteritemSpacing * 0.5) / itemLength)
        let maxRaw = max(numberOfItems * maxSectionCount - 1, 0)
        let itemIndex = max(0, min(raw, maxRaw))
        return LNIndexSection(index: itemIndex % numberOfItems, section: itemIndex / numberOfItems)
    }

    private func calculateOffset(at target: LNIndexSection) -> CGFloat {
        guard numberOfItems > 0, let layout = effectiveLayout() else { return 0 }

        let edge = isInfiniteLoop ? layout.sectionInset : layout.onlyOneSectionInset
        let itemLength = (resolvedScrollDirection() == .horizontal ? transformLayout.itemSize.width : transformLayout.itemSize.height) + transformLayout.minimumInteritemSpacing
        let viewportLength = resolvedScrollDirection() == .horizontal ? collectionView.bounds.width : collectionView.bounds.height

        let leftOrTop = resolvedScrollDirection() == .horizontal ? edge.left : edge.top
        let rightOrBottom = resolvedScrollDirection() == .horizontal ? edge.right : edge.bottom

        let logical = CGFloat(target.index + target.section * numberOfItems)
        let normal = leftOrTop + itemLength * logical - transformLayout.minimumInteritemSpacing * 0.5 - (viewportLength - itemLength) * 0.5

        if !isInfiniteLoop, target.index == numberOfItems - 1 {
            if resolvedScrollDirection() == .horizontal, !layout.itemHorizontalCenter {
                let alignedEnd = leftOrTop + itemLength * logical - (viewportLength - itemLength) - transformLayout.minimumInteritemSpacing + rightOrBottom
                return max(alignedEnd, 0)
            }
            if resolvedScrollDirection() == .vertical {
                let alignedEnd = leftOrTop + itemLength * logical - (viewportLength - itemLength) - transformLayout.minimumInteritemSpacing + rightOrBottom
                return max(alignedEnd, 0)
            }
        }

        return max(normal, 0)
    }

    private func resetPagerView(at index: Int) {
        var targetIndex = index
        if didLayout, firstScrollIndex >= 0 {
            targetIndex = firstScrollIndex
            firstScrollIndex = -1
        }
        if targetIndex < 0 { return }
        if targetIndex >= numberOfItems { targetIndex = 0 }

        let section = isInfiniteLoop ? (maxSectionCount / 3) : 0
        scrollToItem(at: LNIndexSection(index: targetIndex, section: section), animated: false)
        if !isInfiniteLoop, indexSection.index < 0 {
            scrollViewDidScroll(collectionView)
        }
    }

    private func recyclePagerViewIfNeeded() {
        guard isInfiniteLoop else { return }
        if indexSection.section > maxSectionCount - minSectionCount || indexSection.section < minSectionCount {
            resetPagerView(at: indexSection.index)
        }
    }

    private func resolvedScrollDirection() -> LNCyclePagerScrollDirection {
        effectiveLayout()?.scrollDirection ?? .horizontal
    }

}

extension LNCyclePager: UICollectionViewDataSource {
    public func numberOfSections(in collectionView: UICollectionView) -> Int {
        isInfiniteLoop ? maxSectionCount : 1
    }

    public func collectionView(_ collectionView: UICollectionView, numberOfItemsInSection section: Int) -> Int {
        numberOfItems = max(0, numberOfItemsProvider?() ?? 0)
        return numberOfItems
    }

    public func collectionView(_ collectionView: UICollectionView, cellForItemAt indexPath: IndexPath) -> UICollectionViewCell {
        dequeueSection = indexPath.section
        guard let cellProvider else {
            assertionFailure("LNCyclePager cellProvider is nil")
            return UICollectionViewCell()
        }
        return cellProvider(self, indexPath.item)
    }
}

extension LNCyclePager: UICollectionViewDelegateFlowLayout {
    public func collectionView(_ collectionView: UICollectionView, layout collectionViewLayout: UICollectionViewLayout, insetForSectionAt section: Int) -> UIEdgeInsets {
        guard let layout = effectiveLayout() else { return .zero }
        if !isInfiniteLoop {
            return layout.onlyOneSectionInset
        }
        if section == 0 {
            return layout.firstSectionInset
        }
        if section == maxSectionCount - 1 {
            return layout.lastSectionInset
        }
        return layout.middleSectionInset
    }

    public func collectionView(_ collectionView: UICollectionView, didSelectItemAt indexPath: IndexPath) {
        guard let cell = collectionView.cellForItem(at: indexPath) else { return }
        didSelectItem?(cell, indexPath.item)
        didSelectItemAtIndexSection?(cell, LNIndexSection(index: indexPath.item, section: indexPath.section))
    }
}

extension LNCyclePager: UIScrollViewDelegate {
    public func scrollViewDidScroll(_ scrollView: UIScrollView) {
        guard didLayout else { return }
        let newIndexSection = calculateIndexSection(with: scrollView.contentOffset)
        guard numberOfItems > 0, isValid(newIndexSection) else { return }
        let old = indexSection
        indexSection = newIndexSection

        didScroll?()

        if old != indexSection {
            didScrollFromIndexToIndex?(max(old.index, 0), indexSection.index)
            boundPageControl?.setCurrentPage(indexSection.index, animate: true)
        }
    }

    public func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
        if autoScrollInterval > 0 {
            stopTimer()
        }
        beginDragIndexSection = calculateIndexSection(with: scrollView.contentOffset)
    }

    public func scrollViewWillEndDragging(_ scrollView: UIScrollView, withVelocity velocity: CGPoint, targetContentOffset: UnsafeMutablePointer<CGPoint>) {
        let horizontal = resolvedScrollDirection() == .horizontal
        let velocityValue = horizontal ? abs(velocity.x) : abs(velocity.y)
        if velocityValue < 0.35 || beginDragIndexSection != indexSection {
            let target = calculateOffset(at: indexSection)
            if horizontal {
                targetContentOffset.pointee.x = target
            } else {
                targetContentOffset.pointee.y = target
            }
            return
        }

        let direction: LNPagerScrollDirection
        if horizontal {
            let isLeft = (scrollView.contentOffset.x < 0 && targetContentOffset.pointee.x <= 0) ||
                (targetContentOffset.pointee.x < scrollView.contentOffset.x &&
                 scrollView.contentOffset.x < scrollView.contentSize.width - scrollView.frame.width)
            direction = isLeft ? .left : .right
        } else {
            let isUp = (scrollView.contentOffset.y < 0 && targetContentOffset.pointee.y <= 0) ||
                (targetContentOffset.pointee.y < scrollView.contentOffset.y &&
                 scrollView.contentOffset.y < scrollView.contentSize.height - scrollView.frame.height)
            direction = isUp ? .left : .right
        }

        let targetIndexSection = nearlyIndexSection(from: indexSection, direction: direction)
        let target = calculateOffset(at: targetIndexSection)
        if horizontal {
            targetContentOffset.pointee.x = target
        } else {
            targetContentOffset.pointee.y = target
        }
    }

    public func scrollViewDidEndDragging(_ scrollView: UIScrollView, willDecelerate decelerate: Bool) {
        if autoScrollInterval > 0 {
            startTimerIfNeeded()
        }
    }

    public func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        recyclePagerViewIfNeeded()
    }

    public func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) {
        recyclePagerViewIfNeeded()
    }
}

extension LNCyclePager: LNCyclePagerTransformLayoutDelegate {
    public func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, initialize attributes: UICollectionViewLayoutAttributes) {
        initializeTransformAttributes?(attributes)
    }

    public func pagerTransformLayout(_ layout: LNCyclePagerTransformLayout, apply attributes: UICollectionViewLayoutAttributes) {
        applyTransformAttributes?(attributes)
    }
}
