//
//  MPVOGLView.swift
//  Yattee
//
//  NSView that hosts MPVOpenGLLayer and manages CADisplayLink for macOS.
//  Moves all rendering off the main thread for smooth UI during video playback.
//

#if os(macOS)

import AppKit
import CoreMedia
import CoreVideo
import Libmpv
import QuartzCore

// MARK: - MPVOGLView

/// View for MPV video rendering on macOS.
/// Hosts an MPVOpenGLLayer and manages CADisplayLink for vsync timing.
final class MPVOGLView: NSView {
    // MARK: - Properties

    /// The OpenGL layer that handles rendering.
    /// `nil` when OpenGL is unavailable (e.g. a GPU-less virtual machine); the
    /// view then renders nothing and `setup(with:)` throws instead of crashing.
    private(set) var videoLayer: MPVOpenGLLayer?

    /// Reference to the MPV client.
    private weak var mpvClient: MPVClient?

    /// CADisplayLink for frame timing and vsync (macOS 14+).
    private var displayLink: CADisplayLink?

    /// Whether the view has been uninitialized.
    private var isUninited = false

    /// Lock for thread-safe access to isUninited.
    private let uninitLock = NSLock()

    // MARK: - First Frame Tracking

    /// Tracks whether MPV has signaled it has a frame ready to render.
    var mpvHasFrameReady = false

    /// Callback when first frame is rendered.
    var onFirstFrameRendered: (() -> Void)? {
        get { videoLayer?.onFirstFrameRendered }
        set { videoLayer?.onFirstFrameRendered = newValue }
    }

    // MARK: - Video Info

    /// Video frame rate from MPV (for debug overlay).
    var videoFPS: Double = 60.0

    /// Actual display link frame rate.
    var displayLinkActualFPS: Double = 60.0

    /// Current display link target frame rate (for debug overlay).
    var displayLinkTargetFPS: Double {
        displayLinkActualFPS
    }

    // MARK: - PiP Properties (forwarded to layer)

    /// Whether to capture frames for PiP.
    var captureFramesForPiP: Bool {
        get { videoLayer?.captureFramesForPiP ?? false }
        set { videoLayer?.captureFramesForPiP = newValue }
    }

    /// Whether PiP is currently active.
    var isPiPActive: Bool {
        get { videoLayer?.isPiPActive ?? false }
        set { videoLayer?.isPiPActive = newValue }
    }

    /// Callback when a new frame is ready for PiP.
    var onFrameReady: ((CVPixelBuffer, CMTime) -> Void)? {
        get { videoLayer?.onFrameReady }
        set { videoLayer?.onFrameReady = newValue }
    }

    /// Video content width (actual video dimensions for letterbox cropping).
    var videoContentWidth: Int {
        get { videoLayer?.videoContentWidth ?? 0 }
        set { videoLayer?.videoContentWidth = newValue }
    }

    /// Video content height (actual video dimensions for letterbox cropping).
    var videoContentHeight: Int {
        get { videoLayer?.videoContentHeight ?? 0 }
        set { videoLayer?.videoContentHeight = newValue }
    }

    // MARK: - Initialization

    override init(frame frameRect: NSRect) {
        super.init(frame: frameRect)
        commonInit()
    }

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

    /// Convenience initializer with zero frame.
    convenience init() {
        self.init(frame: .zero)
    }

    private func commonInit() {
        // Set up layer-backed view
        wantsLayer = true

        // Create the OpenGL layer. This can fail on machines without a usable
        // GPU (e.g. virtual machines), in which case the view renders nothing
        // and setup(with:) will throw rather than crashing the app at launch.
        if let videoLayer = MPVOpenGLLayer(videoView: self) {
            self.videoLayer = videoLayer
            layer = videoLayer
            videoLayer.contentsScale = NSScreen.main?.backingScaleFactor ?? 2.0
        } else {
            LoggingService.shared.error("MPVOGLView: OpenGL unavailable - video rendering disabled", category: .mpv)
        }

        // Configure view properties
        autoresizingMask = [.width, .height]
    }

    deinit {
        uninit()
    }

    // MARK: - Setup

    /// Set up with an MPV client.
    func setup(with client: MPVClient) throws {
        self.mpvClient = client

        // No layer means OpenGL was unavailable at init (e.g. GPU-less VM).
        guard let videoLayer else {
            throw MPVRenderError.openGLSetupFailed
        }

        // Set up the layer
        try videoLayer.setup(with: client)

        // Start display link
        startDisplayLink()
    }

    /// Async setup variant.
    func setupAsync(with client: MPVClient) async throws {
        try setup(with: client)
    }

    // MARK: - View Lifecycle

    override var isOpaque: Bool { true }

    override func viewDidMoveToWindow() {
        super.viewDidMoveToWindow()

        if let window {
            // Recreate display link for new window
            stopDisplayLink()
            startDisplayLink()

            // Update contents scale for new window. Disable implicit actions so
            // Core Animation never builds a presentation copy of the GL layer
            // for this change (crashes on macOS 27 beta).
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            videoLayer?.contentsScale = window.backingScaleFactor
            CATransaction.commit()

            // Reattaching (e.g. macOS player sheet dismissed via ESC then re-expanded)
            // detaches this shared view without stopping playback. macOS drawing is
            // pull-based, so force a redraw to repaint the current frame — otherwise
            // the layer stays black until MPV emits a new frame (never, if paused).
            resumeRendering()
        }
    }

    override func viewDidChangeBackingProperties() {
        super.viewDidChangeBackingProperties()

        // Update contents scale when backing properties change. Disable implicit
        // actions so Core Animation never builds a presentation copy of the GL
        // layer for this change (crashes on macOS 27 beta).
        if let scale = window?.backingScaleFactor {
            CATransaction.begin()
            CATransaction.setDisableActions(true)
            videoLayer?.contentsScale = scale
            CATransaction.commit()
        }

        // Update display refresh rate
        updateDisplayRefreshRate()
    }

    override func setFrameSize(_ newSize: NSSize) {
        super.setFrameSize(newSize)

        // If a forced draw was requested while the layer had no size/drawable
        // (shared view re-parented into the deferred expanded sheet), repaint
        // on the first non-zero layout. Gated on hasPendingForcedDraw so
        // ordinary resizes don't trigger redundant redraws.
        guard newSize.width > 0, newSize.height > 0,
              let videoLayer, videoLayer.hasPendingForcedDraw else { return }
        LoggingService.shared.debug("MPVOGLView: retrying pending forced draw after resize to \(newSize)", category: .mpv)
        videoLayer.update(force: true)
    }

    override func draw(_ dirtyRect: NSRect) {
        // No-op - the layer handles all drawing
    }

    // MARK: - CADisplayLink Management

    func startDisplayLink() {
        guard displayLink == nil else { return }

        // Create display link using modern API (macOS 14+)
        displayLink = displayLink(target: self, selector: #selector(displayLinkFired(_:)))
        displayLink?.add(to: .main, forMode: .common)

        // Update refresh rate info
        updateDisplayRefreshRate()

        LoggingService.shared.debug("MPVOGLView: display link started", category: .mpv)
    }

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

        LoggingService.shared.debug("MPVOGLView: display link stopped", category: .mpv)
    }

    @objc private func displayLinkFired(_ sender: CADisplayLink) {
        // Check if uninited (thread-safe)
        uninitLock.lock()
        let uninited = isUninited
        uninitLock.unlock()

        guard !uninited else { return }

        // Report frame swap to MPV for vsync timing
        mpvClient?.reportSwap()
    }

    /// Update display link for the current display.
    func updateDisplayLink() {
        // With CADisplayLink, we just need to update the refresh rate info
        updateDisplayRefreshRate()
    }

    /// Update the cached display refresh rate.
    private func updateDisplayRefreshRate() {
        guard let screen = window?.screen else {
            displayLinkActualFPS = 60.0
            return
        }

        // Get refresh rate from screen
        displayLinkActualFPS = Double(screen.maximumFramesPerSecond)
        if displayLinkActualFPS <= 0 {
            displayLinkActualFPS = 60.0
        }

        LoggingService.shared.debug("MPVOGLView: display refresh rate: \(displayLinkActualFPS) Hz", category: .mpv)
    }

    // MARK: - Public Methods

    /// Reset first frame tracking (call when loading new content).
    func resetFirstFrameTracking() {
        mpvHasFrameReady = false
        videoLayer?.resetFirstFrameTracking()
    }

    /// Thread-safe snapshot of the layer's render-health counters.
    func renderHealthSnapshot() -> MPVRenderHealth? {
        videoLayer?.renderHealthSnapshot()
    }

    /// Human-readable description of where this shared view is currently
    /// attached, for render-health diagnostics. Main thread only.
    var attachmentDescription: String {
        let superviewDesc: String
        if let container = superview as? MPVContainerNSView {
            superviewDesc = "container=\(container.shortID)"
        } else if let superview {
            superviewDesc = "superview=\(type(of: superview))"
        } else {
            superviewDesc = "superview=nil"
        }
        let windowDesc = window.map { "window=#\($0.windowNumber) visible=\($0.isVisible)" } ?? "window=nil"
        return "\(superviewDesc) \(windowDesc) bounds=\(Int(bounds.width))x\(Int(bounds.height))"
    }

    /// Clear the view to black.
    func clearToBlack() {
        videoLayer?.clearToBlack()
    }

    /// Pause rendering.
    func pauseRendering() {
        // For now, just stop triggering updates
        // The layer will still respond to explicit update() calls
    }

    /// Resume rendering.
    func resumeRendering() {
        videoLayer?.update(force: true)
    }

    /// Update cached time position for PiP timestamps.
    func updateTimePosition(_ time: Double) {
        videoLayer?.updateTimePosition(time)
    }

    /// Clear the main view for PiP transition (stub for now).
    func clearMainViewForPiP() {
        clearToBlack()
    }

    /// Update PiP target render size - forces recreation of PiP capture resources.
    func updatePiPTargetSize(_ size: CMVideoDimensions) {
        videoLayer?.updatePiPTargetSize(size)
    }

    // MARK: - Cleanup

    /// Uninitialize the view and release resources.
    func uninit() {
        uninitLock.lock()
        defer { uninitLock.unlock() }

        guard !isUninited else { return }
        isUninited = true

        stopDisplayLink()
        videoLayer?.uninit()

        // Note: onFirstFrameRendered and onFrameReady are forwarded to videoLayer,
        // and videoLayer.uninit() clears them
    }
}

#endif
