import Foundation
#if canImport(AppKit)
import AppKit
import Combine

// MARK: - Sheet Music View

/// Custom NSView that renders sheet music notation and handles measure selection.
/// Placed inside a scroll view and wrapped with NSViewRepresentable for SwiftUI.
class SheetMusicView: NSView {

    // MARK: - Properties

    var song: Song? {
        didSet {
            recalculateLayout()
            needsDisplay = true
        }
    }

    var selection: Selection = .none {
        didSet { needsDisplay = true }
    }

    var playbackMeasure: Int = -1 {
        didSet { needsDisplay = true }
    }

    var playbackBeatFraction: Double = 0.0 {
        didSet { needsDisplay = true }
    }

    /// Callback when selection changes (for updating the SwiftUI state)
    var onSelectionChanged: ((Selection) -> Void)?

    // MARK: - Layout

    private var layout: SheetMusicLayout = SheetMusicLayout(
        systems: [], measureFrames: [], totalSize: .zero, measuresPerSystem: 0
    )

    // MARK: - Renderers

    private let staffRenderer = StaffRenderer()
    private let noteRenderer = NoteRenderer()
    private let selectionRenderer = SelectionRenderer()

    // MARK: - Mouse tracking

    private var isDragging = false
    private var dragAnchorMeasure: Int?

    // MARK: - Initialization

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

    required init?(coder: NSCoder) {
        super.init(coder: coder)
        setupView()
    }

    private func setupView() {
        wantsLayer = true
        layer?.backgroundColor = NSColor.white.cgColor
    }

    // MARK: - Layout

    private func recalculateLayout() {
        guard let song = song else {
            layout = SheetMusicLayout(systems: [], measureFrames: [], totalSize: .zero, measuresPerSystem: 0)
            return
        }

        // Use the enclosing scroll view's width if available, otherwise our bounds
        let viewWidth: CGFloat
        if let scrollView = enclosingScrollView {
            viewWidth = max(scrollView.contentView.bounds.width, 600)
        } else {
            viewWidth = max(bounds.width, 600)
        }

        layout = LayoutCalculator.calculateLayout(
            song: song,
            viewWidth: viewWidth
        )

        // Update frame size so the scroll view knows the content size
        setFrameSize(NSSize(width: viewWidth, height: layout.totalSize.height))
        invalidateIntrinsicContentSize()
    }

    override func viewDidMoveToSuperview() {
        super.viewDidMoveToSuperview()
        // Recalculate once we're in the scroll view and have a real width
        if superview != nil {
            recalculateLayout()
        }
    }

    override func resize(withOldSuperviewSize oldSize: NSSize) {
        super.resize(withOldSuperviewSize: oldSize)
        recalculateLayout()
    }

    override var intrinsicContentSize: NSSize {
        return layout.totalSize
    }

    override var isFlipped: Bool {
        return true  // Use top-left origin (standard for document views)
    }

    // MARK: - Drawing

    override func draw(_ dirtyRect: NSRect) {
        guard let context = NSGraphicsContext.current?.cgContext else { return }
        guard let song = song else {
            drawEmptyState(in: context, rect: bounds)
            return
        }

        // Background
        context.setFillColor(NSColor.white.cgColor)
        context.fill(bounds)

        // Draw selection highlight (behind everything)
        selectionRenderer.drawSelection(selection, layout: layout, in: context)

        // Draw each system
        for system in layout.systems {
            drawSystem(system, song: song, in: context)
        }

        // Draw playback position
        if playbackMeasure >= 0 {
            selectionRenderer.drawPlaybackPosition(
                measure: playbackMeasure,
                beatFraction: playbackBeatFraction,
                layout: layout,
                in: context
            )
        }
    }

    private func drawSystem(_ system: StaffSystem, song: Song, in context: CGContext) {
        let L = AppConstants.Layout.self

        for (partIndex, part) in song.parts.enumerated() {
            let staffY = system.staffY(forPartIndex: partIndex)

            // Draw staff lines across the full system width
            let systemStartX = system.measures.first?.rect.minX ?? L.leftMargin
            let systemEndX = system.measures.last?.rect.maxX ?? bounds.width

            staffRenderer.drawStaffLines(
                in: context,
                x: systemStartX - L.leftMargin,
                y: staffY,
                width: systemEndX - systemStartX + L.leftMargin
            )

            // Draw clef at the start of each system
            if partIndex < song.parts.count {
                // Use treble clef for higher voices, bass clef for lower
                let useBassClef = partIndex >= song.parts.count - 1 && song.parts.count >= 4
                if useBassClef {
                    staffRenderer.drawBassClef(in: context, x: systemStartX - L.leftMargin + 4, y: staffY)
                } else {
                    staffRenderer.drawTrebleClef(in: context, x: systemStartX - L.leftMargin + 4, y: staffY)
                }
            }

            // Draw key and time signature on first system only
            if system.systemIndex == 0 {
                var sigX = systemStartX - L.leftMargin + 36
                sigX = staffRenderer.drawKeySignature(
                    in: context,
                    keySignature: song.keySignature,
                    x: sigX,
                    y: staffY
                )
                staffRenderer.drawTimeSignature(
                    in: context,
                    timeSignature: song.timeSignature,
                    x: sigX + 4,
                    y: staffY
                )
            }

            // Draw notes for each measure in this system
            let partColor = PartColors.color(forIndex: part.colorIndex)

            for measureFrame in system.measures {
                let measureIdx = measureFrame.measureIndex
                guard measureIdx < part.measures.count else { continue }

                let measure = part.measures[measureIdx]
                drawMeasureNotes(
                    measure,
                    in: measureFrame.rect,
                    staffY: staffY,
                    color: partColor,
                    context: context
                )

                // Draw bar line at end of measure
                staffRenderer.drawBarLine(
                    in: context,
                    x: measureFrame.rect.maxX,
                    y: staffY
                )
            }

            // Draw double bar line at end of last measure
            if let lastFrame = system.measures.last,
               lastFrame.measureIndex == song.totalMeasures - 1 {
                staffRenderer.drawDoubleBarLine(
                    in: context,
                    x: lastFrame.rect.maxX,
                    y: staffY
                )
            }
        }
    }

    private func drawMeasureNotes(
        _ measure: Measure,
        in rect: CGRect,
        staffY: CGFloat,
        color: NSColor,
        context: CGContext
    ) {
        let notes = measure.notes
        guard !notes.isEmpty else { return }

        // Space notes proportionally by duration within the measure
        let totalBeats = notes.reduce(0.0) { $0 + $1.beats }
        guard totalBeats > 0 else { return }

        let padding: CGFloat = 8.0
        let availableWidth = rect.width - padding * 2
        var currentX = rect.minX + padding
        var beatsSoFar: Double = 0

        for note in notes {
            let noteWidth = CGFloat(note.beats / totalBeats) * availableWidth
            let noteX = currentX + noteWidth * 0.3  // offset slightly for visual balance

            noteRenderer.drawNote(
                note,
                at: noteX,
                staffTopY: staffY,
                color: color,
                in: context
            )

            // Draw ledger lines if needed
            if let pitch = note.pitch {
                let noteY = noteRenderer.noteY(for: pitch, staffTopY: staffY)
                staffRenderer.drawLedgerLines(
                    in: context,
                    noteY: noteY,
                    staffTopY: staffY,
                    noteX: noteX
                )
            }

            currentX += noteWidth
            beatsSoFar += note.beats
        }
    }

    private func drawEmptyState(in context: CGContext, rect: CGRect) {
        context.setFillColor(NSColor.windowBackgroundColor.cgColor)
        context.fill(rect)

        let text = "Open a song file to begin" as NSString
        let attrs: [NSAttributedString.Key: Any] = [
            .font: NSFont.systemFont(ofSize: 18),
            .foregroundColor: NSColor.secondaryLabelColor
        ]
        let size = text.size(withAttributes: attrs)
        let point = CGPoint(
            x: (rect.width - size.width) / 2,
            y: (rect.height - size.height) / 2
        )
        text.draw(at: point, withAttributes: attrs)
    }

    // MARK: - Mouse handling for selection

    override func mouseDown(with event: NSEvent) {
        // Make this view the first responder so we get key events
        window?.makeFirstResponder(self)

        let point = convert(event.locationInWindow, from: nil)
        guard let measureIndex = layout.measureIndex(at: point) else {
            // Clicked outside any measure — clear everything
            selection.clear()
            onSelectionChanged?(selection)
            needsDisplay = true
            return
        }

        if event.modifierFlags.contains(.shift) {
            // Shift-click: extend selection from cursor to here
            selection.shiftClick(measure: measureIndex)
        } else {
            // Regular click: just move the cursor (no selection yet)
            // If they drag, we'll start a selection in mouseDragged
            selection.click(measure: measureIndex)
            isDragging = true
            dragAnchorMeasure = measureIndex
        }

        onSelectionChanged?(selection)
        needsDisplay = true
    }

    override func mouseDragged(with event: NSEvent) {
        guard isDragging, let anchor = dragAnchorMeasure else { return }
        let point = convert(event.locationInWindow, from: nil)
        if let measureIndex = layout.measureIndex(at: point) {
            // Once they actually drag to a different measure, start a selection
            if measureIndex != anchor {
                selection.anchor = anchor
                selection.cursor = measureIndex
            } else {
                // Still on the same measure — just cursor, no selection
                selection.click(measure: anchor)
            }
            onSelectionChanged?(selection)
            needsDisplay = true
        }
    }

    override func mouseUp(with event: NSEvent) {
        isDragging = false
        dragAnchorMeasure = nil
    }

    override var acceptsFirstResponder: Bool { true }
}

#endif
