import UIKit
import CoreText
import MusicModel
import ScoreLayout

/// Draws an EngravedScore with CoreGraphics + the Bravura font, and hosts
/// the playback cursor and selection highlight as separate layers so that
/// 60–120 Hz cursor updates never redraw the engraving.
final class ScoreView: UIView {

    // MARK: - Font registration

    private static let registerFont: Void = {
        guard let url = BravuraResources.fontURL else {
            assertionFailure("Bravura.otf missing from ScoreLayout resources")
            return
        }
        CTFontManagerRegisterFontsForURL(url as CFURL, .process, nil)
    }()

    // MARK: - Content

    var engraved: EngravedScore? {
        didSet { invalidateContent() }
    }

    /// Points per staff space (zoom). 10pt/space ≈ comfortable phone size.
    var pointsPerSpace: CGFloat = 10 {
        didSet { invalidateContent() }
    }

    /// Part to emphasize (others dim slightly).
    var highlightedPartIndex: Int? {
        didSet { setNeedsDisplay() }
    }

    /// Selected measure range (highlight layer).
    var selectedMeasures: ClosedRange<Int>? {
        didSet { updateSelectionLayer() }
    }

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

    private let cursorLayer = CALayer()
    private let selectionLayer = CAShapeLayer()

    override init(frame: CGRect) {
        super.init(frame: frame)
        _ = ScoreView.registerFont
        backgroundColor = .systemBackground
        contentScaleFactor = UIScreen.main.scale

        selectionLayer.fillColor = UIColor.systemBlue.withAlphaComponent(0.13).cgColor
        layer.addSublayer(selectionLayer)

        cursorLayer.backgroundColor = UIColor.systemRed.cgColor
        cursorLayer.opacity = 0
        layer.addSublayer(cursorLayer)

        let tap = UITapGestureRecognizer(target: self, action: #selector(handleTap(_:)))
        addGestureRecognizer(tap)
    }

    required init?(coder: NSCoder) { fatalError("not used") }

    private func invalidateContent() {
        invalidateIntrinsicContentSize()
        updateSelectionLayer()
        setNeedsDisplay()
    }

    override var intrinsicContentSize: CGSize {
        guard let engraved else { return .zero }
        return CGSize(
            width: engraved.width * pointsPerSpace + 16,
            height: engraved.height * pointsPerSpace + 24
        )
    }

    /// Content origin offset in points.
    private var origin: CGPoint { CGPoint(x: 8, y: 12) }

    // MARK: - Cursor

    /// Move the playback cursor to a fractional tick (nil hides it).
    /// Called from a CADisplayLink — touches only the cursor layer.
    func setCursor(fractionalTick: Double?) {
        CATransaction.begin()
        CATransaction.setDisableActions(true)
        defer { CATransaction.commit() }

        guard let engraved, let tick = fractionalTick,
              let system = engraved.system(containingTick: tick),
              let x = system.cursorX(forTick: tick) else {
            cursorLayer.opacity = 0
            return
        }
        let s = pointsPerSpace
        cursorLayer.opacity = 1
        cursorLayer.frame = CGRect(
            x: origin.x + x * s - 0.75,
            y: origin.y + system.yOrigin * s,
            width: 1.5,
            height: system.height * s
        )
    }

    /// The rect (in view coords) of the system containing a tick — for auto-scroll.
    func systemRect(containingTick tick: Double) -> CGRect? {
        guard let engraved, let system = engraved.system(containingTick: tick) else { return nil }
        let s = pointsPerSpace
        return CGRect(
            x: 0, y: origin.y + system.yOrigin * s,
            width: bounds.width, height: system.height * s
        )
    }

    // MARK: - Selection

    private func updateSelectionLayer() {
        guard let engraved, let selection = selectedMeasures else {
            selectionLayer.path = nil
            return
        }
        let s = pointsPerSpace
        let path = CGMutablePath()
        for system in engraved.systems {
            for measure in system.measures where selection.contains(measure.index) {
                path.addRect(CGRect(
                    x: origin.x + measure.xStart * s,
                    y: origin.y + system.yOrigin * s,
                    width: (measure.xEnd - measure.xStart) * s,
                    height: system.height * s
                ))
            }
        }
        selectionLayer.path = path
    }

    @objc private func handleTap(_ gesture: UITapGestureRecognizer) {
        guard let engraved else { return }
        let point = gesture.location(in: self)
        let s = pointsPerSpace
        let spacePoint = (
            x: (point.x - origin.x) / s,
            y: (point.y - origin.y) / s
        )
        if let hit = engraved.measure(at: (x: spacePoint.x, y: spacePoint.y)) {
            onMeasureTapped?(hit.measure)
        }
    }

    // MARK: - Drawing

    override func draw(_ rect: CGRect) {
        guard let context = UIGraphicsGetCurrentContext(), let engraved else { return }
        let s = pointsPerSpace
        let fontSize = 4 * s   // SMuFL em = 4 staff spaces
        let font = CTFontCreateWithName(BravuraResources.postScriptName as CFString, fontSize, nil)

        context.translateBy(x: origin.x, y: origin.y)

        for system in engraved.systems {
            let systemRect = CGRect(
                x: 0, y: system.yOrigin * s,
                width: system.width * s, height: system.height * s
            )
            guard rect.insetBy(dx: 0, dy: -50).intersects(
                systemRect.offsetBy(dx: origin.x, dy: origin.y)
            ) else { continue }

            context.saveGState()
            context.translateBy(x: 0, y: system.yOrigin * s)

            // Tint band behind the highlighted part's staff.
            if let highlighted = highlightedPartIndex,
               let staff = system.staves.first(where: { $0.partIndex == highlighted }) {
                context.setFillColor(UIColor.systemYellow.withAlphaComponent(0.14).cgColor)
                context.fill(CGRect(
                    x: staff.xStart * s,
                    y: (staff.yTop - 1.5) * s,
                    width: (staff.xEnd - staff.xStart) * s,
                    height: 7 * s
                ))
            }

            // Lines (staff lines, stems, barlines, ledger lines).
            for line in system.lines {
                context.setStrokeColor(UIColor.label.cgColor)
                context.setLineWidth(line.thickness * s)
                context.move(to: CGPoint(x: line.x1 * s, y: line.y1 * s))
                context.addLine(to: CGPoint(x: line.x2 * s, y: line.y2 * s))
                context.strokePath()
            }

            // Beams.
            context.setFillColor(UIColor.label.cgColor)
            for beam in system.beams {
                let t = beam.thickness * s
                let path = CGMutablePath()
                path.move(to: CGPoint(x: beam.x1 * s, y: beam.y1 * s - t / 2))
                path.addLine(to: CGPoint(x: beam.x2 * s, y: beam.y2 * s - t / 2))
                path.addLine(to: CGPoint(x: beam.x2 * s, y: beam.y2 * s + t / 2))
                path.addLine(to: CGPoint(x: beam.x1 * s, y: beam.y1 * s + t / 2))
                context.addPath(path)
                context.fillPath()
            }

            // Ties.
            context.setStrokeColor(UIColor.label.cgColor)
            for tie in system.ties {
                let lift: CGFloat = (tie.bowsUp ? -1 : 1) * 0.9 * s
                let path = CGMutablePath()
                path.move(to: CGPoint(x: tie.x1 * s, y: tie.y1 * s))
                path.addQuadCurve(
                    to: CGPoint(x: tie.x2 * s, y: tie.y2 * s),
                    control: CGPoint(x: (tie.x1 + tie.x2) / 2 * s, y: tie.y1 * s + lift)
                )
                context.setLineWidth(0.14 * s)
                context.addPath(path)
                context.strokePath()
            }

            // Glyphs — drawn via CoreText at baseline = glyph y.
            for placement in system.glyphs {
                let attributes: [NSAttributedString.Key: Any] = [
                    .font: font as Any,
                    .foregroundColor: UIColor.label.cgColor,
                ]
                let text = NSAttributedString(string: String(placement.glyph.character), attributes: attributes)
                let ctLine = CTLineCreateWithAttributedString(text)
                context.saveGState()
                // Flip for CoreText (y grows up there). ORDER MATTERS:
                // textPosition writes into the text matrix, so it must be
                // set AFTER textMatrix or the position is wiped to (0,0).
                context.textMatrix = CGAffineTransform(scaleX: 1, y: -1)
                context.textPosition = CGPoint(x: placement.x * s, y: placement.y * s)
                CTLineDraw(ctLine, context)
                context.restoreGState()
            }

            // Tuplet numbers.
            for mark in system.tuplets {
                let digits = String(mark.number).compactMap { $0.wholeNumberValue }
                for (i, digit) in digits.enumerated() {
                    let glyph = SMuFLGlyph.tupletDigit(digit)
                    let text = NSAttributedString(
                        string: String(glyph.character),
                        attributes: [.font: font as Any, .foregroundColor: UIColor.label.cgColor]
                    )
                    let ctLine = CTLineCreateWithAttributedString(text)
                    context.saveGState()
                    context.textMatrix = CGAffineTransform(scaleX: 1, y: -1)
                    context.textPosition = CGPoint(x: (mark.x + Double(i) * 1.0 - 0.4) * s, y: mark.y * s)
                    CTLineDraw(ctLine, context)
                    context.restoreGState()
                }
            }

            context.restoreGState()
        }
    }
}
