import SwiftUI
import UIKit
import MusicModel
import ScoreLayout

/// SwiftUI host for ScoreView inside a scroll view, with a CADisplayLink
/// cursor driven by a tick provider and gentle auto-scroll that keeps the
/// current system (plus a preview of the next) on screen.
struct ScoreScrollView: UIViewRepresentable {
    var score: Score?
    var highlightedPartIndex: Int?
    var selectedMeasures: ClosedRange<Int>?
    /// Called at display rate; return the current fractional tick or nil.
    var tickProvider: () -> Double?
    var autoScroll: Bool
    var onMeasureTapped: ((Int) -> Void)?

    func makeUIView(context: Context) -> UIScrollView {
        let scrollView = UIScrollView()
        scrollView.alwaysBounceVertical = true

        let scoreView = ScoreView(frame: .zero)
        scrollView.addSubview(scoreView)
        context.coordinator.scoreView = scoreView
        context.coordinator.scrollView = scrollView
        context.coordinator.startDisplayLink()
        return scrollView
    }

    func updateUIView(_ scrollView: UIScrollView, context: Context) {
        let coordinator = context.coordinator
        coordinator.tickProvider = tickProvider
        coordinator.autoScroll = autoScroll

        guard let scoreView = coordinator.scoreView else { return }
        scoreView.onMeasureTapped = onMeasureTapped
        scoreView.highlightedPartIndex = highlightedPartIndex
        scoreView.selectedMeasures = selectedMeasures

        // Re-layout when the score or width changes.
        let width = scrollView.bounds.width
        if width > 0, coordinator.needsLayout(score: score, width: width) {
            if let score {
                let pointsPerSpace: CGFloat = 9
                let spaces = (width - 16) / pointsPerSpace
                scoreView.pointsPerSpace = pointsPerSpace
                scoreView.engraved = LayoutEngine.layout(
                    score: score,
                    options: .init(systemWidth: spaces)
                )
            } else {
                scoreView.engraved = nil
            }
            let size = scoreView.intrinsicContentSize
            scoreView.frame = CGRect(origin: .zero, size: size)
            scrollView.contentSize = size
        }
    }

    func makeCoordinator() -> Coordinator { Coordinator() }

    static func dismantleUIView(_ uiView: UIScrollView, coordinator: Coordinator) {
        coordinator.stopDisplayLink()
    }

    @MainActor
    final class Coordinator {
        var scoreView: ScoreView?
        weak var scrollView: UIScrollView?
        var tickProvider: () -> Double? = { nil }
        var autoScroll = false

        private var displayLink: CADisplayLink?
        private var layoutKey: String?
        private var lastAutoScrollSystemY: CGFloat = -1

        func needsLayout(score: Score?, width: CGFloat) -> Bool {
            let key = "\(score?.title ?? "-")|\(score?.parts.count ?? 0)|\(Int(width))"
            if key != layoutKey {
                layoutKey = key
                return true
            }
            return false
        }

        func startDisplayLink() {
            let link = CADisplayLink(target: self, selector: #selector(tick))
            link.add(to: .main, forMode: .common)
            displayLink = link
        }

        func stopDisplayLink() {
            displayLink?.invalidate()
            displayLink = nil
        }

        @objc private func tick() {
            guard let scoreView else { return }
            let fractionalTick = tickProvider()
            scoreView.setCursor(fractionalTick: fractionalTick)

            guard autoScroll, let tick = fractionalTick,
                  let scrollView, let rect = scoreView.systemRect(containingTick: tick) else { return }
            // Scroll only when the current system changed, keeping it in the
            // top half so the next system is visible below.
            if abs(rect.minY - lastAutoScrollSystemY) > 1 {
                lastAutoScrollSystemY = rect.minY
                let target = max(0, min(
                    rect.minY - 24,
                    scrollView.contentSize.height - scrollView.bounds.height
                ))
                scrollView.setContentOffset(CGPoint(x: 0, y: target), animated: true)
            }
        }
    }
}
