import UIKit

// MARK: - GalleryPageScrollView

/// Horizontal paging scroll view with 3-page recycling (Telegram parity).
/// Uses `isPagingEnabled` for hardware-level page snapping — zero gesture
/// conflicts with per-page UIScrollView zoom.
final class GalleryPageScrollView: UIView, UIScrollViewDelegate {

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

    private(set) var currentPage: Int = 0
    private var totalPages: Int = 0

    private let scrollView = UIScrollView()
    private var pages: [Int: GalleryZoomPage] = [:]
    private var recycledPages: [GalleryZoomPage] = []
    private let pageGap: CGFloat = 20

    /// Monotonic counter to invalidate stale image-load callbacks after page recycling.
    private var pageGeneration: [ObjectIdentifier: UInt64] = [:]
    private var nextGeneration: UInt64 = 0

    /// Callback to load image for a given page index.
    var imageLoader: ((Int, @escaping (UIImage?) -> Void) -> Void)?

    /// Callbacks forwarded from individual pages.
    var onSingleTap: (() -> Void)?
    var onEdgeTap: ((Int) -> Void)?
    var onZoomChanged: ((Bool) -> Void)?

    /// Returns the zoom page for the current index (used by dismiss controller).
    var currentZoomPage: GalleryZoomPage? { pages[currentPage] }

    // MARK: - Init

    override init(frame: CGRect) {
        super.init(frame: frame)
        setupScrollView()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    // MARK: - Setup

    private func setupScrollView() {
        scrollView.isPagingEnabled = true
        scrollView.showsHorizontalScrollIndicator = false
        scrollView.showsVerticalScrollIndicator = false
        scrollView.contentInsetAdjustmentBehavior = .never
        scrollView.delaysContentTouches = false
        scrollView.delegate = self
        scrollView.bounces = true
        addSubview(scrollView)
    }

    // MARK: - Public API

    func configure(totalPages: Int, initialPage: Int) {
        self.totalPages = totalPages
        self.currentPage = initialPage
        layoutScrollView()
        loadVisiblePages()
    }

    func setPage(_ index: Int, animated: Bool) {
        guard index >= 0, index < totalPages else { return }
        let pageWidth = bounds.width + pageGap
        scrollView.setContentOffset(CGPoint(x: CGFloat(index) * pageWidth, y: 0), animated: animated)
        if !animated {
            currentPage = index
            loadVisiblePages()
        }
    }

    /// Disables paging scroll when a page is zoomed.
    func setPagingEnabled(_ enabled: Bool) {
        scrollView.isScrollEnabled = enabled
    }

    // MARK: - Layout

    override func layoutSubviews() {
        super.layoutSubviews()
        layoutScrollView()
    }

    private func layoutScrollView() {
        let pageWidth = bounds.width + pageGap
        scrollView.frame = CGRect(x: 0, y: 0, width: pageWidth, height: bounds.height)
        scrollView.contentSize = CGSize(width: pageWidth * CGFloat(totalPages), height: bounds.height)
        scrollView.contentOffset = CGPoint(x: CGFloat(currentPage) * pageWidth, y: 0)

        for (index, page) in pages {
            page.frame = frameForPage(at: index)
        }
    }

    private func frameForPage(at index: Int) -> CGRect {
        let pageWidth = bounds.width + pageGap
        return CGRect(x: CGFloat(index) * pageWidth, y: 0, width: bounds.width, height: bounds.height)
    }

    // MARK: - Page Management (3-page window)

    private func loadVisiblePages() {
        let visibleIndices = Set((currentPage - 1)...(currentPage + 1)).filter { $0 >= 0 && $0 < totalPages }

        // Recycle pages outside visible window
        for (index, page) in pages where !visibleIndices.contains(index) {
            page.removeFromSuperview()
            page.resetZoom()
            page.image = nil
            pageGeneration.removeValue(forKey: ObjectIdentifier(page))
            recycledPages.append(page)
            pages.removeValue(forKey: index)
        }

        // Create/load visible pages
        for index in visibleIndices where pages[index] == nil {
            let page = dequeuePage()
            page.frame = frameForPage(at: index)
            page.onSingleTap = { [weak self] in self?.onSingleTap?() }
            page.onEdgeTap = { [weak self] dir in self?.handleEdgeTap(dir) }
            page.onZoomChanged = { [weak self] isZoomed in
                self?.scrollView.isScrollEnabled = !isZoomed
                self?.onZoomChanged?(isZoomed)
            }
            scrollView.addSubview(page)
            pages[index] = page
            nextGeneration += 1
            pageGeneration[ObjectIdentifier(page)] = nextGeneration
            loadImage(for: index, into: page)
        }
    }

    private func dequeuePage() -> GalleryZoomPage {
        if let page = recycledPages.popLast() {
            return page
        }
        return GalleryZoomPage(frame: .zero)
    }

    private func loadImage(for index: Int, into page: GalleryZoomPage) {
        let gen = pageGeneration[ObjectIdentifier(page)] ?? 0
        page.setLoadState(.loading)
        imageLoader?(index) { [weak self, weak page] image in
            guard let self, let page else { return }
            guard self.pageGeneration[ObjectIdentifier(page)] == gen else { return }
            if let image {
                page.image = image
            } else {
                page.setLoadState(.failed)
            }
        }
    }

    private func handleEdgeTap(_ direction: Int) {
        let target = currentPage + direction
        guard target >= 0, target < totalPages else { return }
        setPage(target, animated: true)
    }

    // MARK: - UIScrollViewDelegate

    func scrollViewDidScroll(_ scrollView: UIScrollView) {
        let pageWidth = bounds.width + pageGap
        guard pageWidth > 0 else { return }
        let newPage = Int(round(scrollView.contentOffset.x / pageWidth))
        let clamped = max(0, min(newPage, totalPages - 1))

        if clamped != currentPage {
            currentPage = clamped
            loadVisiblePages()
            onPageChanged?(clamped)
        }
    }

    func scrollViewDidEndDecelerating(_ scrollView: UIScrollView) {
        // Reset zoom on non-current pages after scroll settles (keep image intact)
        for (index, page) in pages where index != currentPage {
            page.resetZoomKeepImage()
        }
    }
}
