# Copyright 2026, Jiri Eischmann <jiri@eischmann.cz> and the meshy contributors
# SPDX-License-Identifier: GPL-3.0-or-later

"""AVFoundation-based QR code scanner for macOS."""

import logging

try:
    import objc
    from AVFoundation import (
        AVAuthorizationStatusAuthorized,
        AVAuthorizationStatusNotDetermined,
        AVCaptureDevice,
        AVCaptureDeviceInput,
        AVCaptureSession,
        AVCaptureVideoDataOutput,
        AVMediaTypeVideo,
    )
    from Foundation import NSArray, NSObject
    from libdispatch import DISPATCH_QUEUE_SERIAL, dispatch_queue_create
    from Vision import (
        VNBarcodeSymbologyQR,
        VNDetectBarcodesRequest,
        VNImageRequestHandler,
    )

    AVFOUNDATION_AVAILABLE = True
except ImportError:
    AVFOUNDATION_AVAILABLE = False
    NSObject = object

log = logging.getLogger(__name__)

# Camera and QR scanner constants
_CAMERA_TARGET_FPS = 30
_CAMERA_FRAME_INTERVAL_S = 1.0 / _CAMERA_TARGET_FPS
_QR_SCAN_HINT_DELAY_MS = 8000
_QR_SCAN_TIMEOUT_MS = 30000
_QR_PREVIEW_MAX_SIZE = 480


# Define GObject implementation at module level to avoid GType re-registration
from gi.repository import Gdk, GObject


class _CameraPaintableImpl(GObject.Object, Gdk.Paintable):
    """GObject that implements Gdk.Paintable interface for camera frames."""

    __gtype_name__ = "CameraPaintable"

    def __init__(self, picture_widget):
        super().__init__()
        self._picture_widget = picture_widget  # Store reference to Picture widget
        self._pixel_data = None  # Raw RGBA pixel data (bytes)
        self._width = 640
        self._height = 480
        self._stride = 0
        self._last_update_time = 0
        self._min_frame_interval = _CAMERA_FRAME_INTERVAL_S
        self._frame_count = 0
        # Cache CIContext for rendering
        from Quartz import CIContext

        self._ci_context = CIContext.contextWithOptions_(None)

    def update_frame(self, pixel_buffer):
        """Update paintable with new frame from AVFoundation.

        Args:
            pixel_buffer: CVPixelBuffer from camera
        """
        import time

        # Check if we should stop processing (dialog closing)
        if hasattr(self, "_stopped") and self._stopped:
            return

        self._frame_count += 1

        # Throttle updates - check FIRST before doing any work
        current_time = time.time()
        if current_time - self._last_update_time < self._min_frame_interval:
            return
        self._last_update_time = current_time

        try:
            # Convert CVPixelBuffer directly to pixel data (no texture creation)
            pixel_data = self._pixel_buffer_to_rgba(pixel_buffer)

            if pixel_data:
                self._pixel_data = pixel_data
                # Trigger redraw
                self.emit("invalidate-contents")

                # Also queue draw on Picture widget
                if self._picture_widget and not (hasattr(self, "_stopped") and self._stopped):
                    try:
                        self._picture_widget.queue_draw()
                    except Exception:
                        # Widget may be destroyed
                        pass
        except Exception as e:
            log.error(f"Failed to update frame: {e}", exc_info=True)

    def stop(self):
        """Stop processing frames."""
        self._stopped = True
        self._picture_widget = None

    def _pixel_buffer_to_rgba(self, pixel_buffer):
        """Convert CVPixelBuffer to raw BGRA pixel data for Cairo.

        Returns dict with pixel data info or None on error.
        """
        from Quartz import CIImage, kCIFormatBGRA8
        from Quartz.CoreGraphics import CGPoint, CGRect, CGSize
        from Quartz.CoreVideo import CVPixelBufferGetHeight, CVPixelBufferGetWidth

        try:
            # Get dimensions
            width = CVPixelBufferGetWidth(pixel_buffer)
            height = CVPixelBufferGetHeight(pixel_buffer)

            if width <= 0 or height <= 0:
                return None

            # Reduce size for performance
            max_size = _QR_PREVIEW_MAX_SIZE
            if width > max_size or height > max_size:
                scale = max_size / max(width, height)
                width = int(width * scale)
                height = int(height * scale)

            # Calculate stride (4-byte aligned)
            stride = width * 4
            buffer_size = stride * height

            # Reuse pixel data buffer if same size
            if not hasattr(self, "_pixel_buffer") or len(self._pixel_buffer) != buffer_size:
                self._pixel_buffer = bytearray(buffer_size)

            # Create CIImage from pixel buffer
            ci_image = CIImage.imageWithCVPixelBuffer_(pixel_buffer)

            # Render to pixel buffer using cached CIContext
            # Use BGRA8 format to match Cairo's ARGB32 byte order
            render_rect = CGRect(CGPoint(0, 0), CGSize(width, height))
            self._ci_context.render_toBitmap_rowBytes_bounds_format_colorSpace_(
                ci_image, self._pixel_buffer, stride, render_rect, kCIFormatBGRA8, None
            )

            # Store dimensions
            self._width = width
            self._height = height
            self._stride = stride

            return {
                "data": bytes(self._pixel_buffer),
                "width": width,
                "height": height,
                "stride": stride,
            }

        except Exception as e:
            log.error(f"Error converting pixel buffer: {e}", exc_info=True)
            return None

    # GdkPaintable interface implementation
    def do_snapshot(self, snapshot, width, height):
        """Render the paintable using Cairo."""
        if not self._pixel_data:
            return

        try:
            import cairo
            from gi.repository import Graphene

            # Create Cairo ImageSurface from pixel data
            # Use ARGB32 format which matches BGRA8 byte order
            image_surface = cairo.ImageSurface.create_for_data(
                bytearray(self._pixel_data["data"]),
                cairo.FORMAT_ARGB32,
                self._pixel_data["width"],
                self._pixel_data["height"],
                self._pixel_data["stride"],
            )

            # Get Cairo context from snapshot
            cr = snapshot.append_cairo(Graphene.Rect().init(0, 0, width, height))

            # Scale to fit the snapshot area
            scale_x = width / self._pixel_data["width"]
            scale_y = height / self._pixel_data["height"]
            cr.scale(scale_x, scale_y)

            # Draw the image
            cr.set_source_surface(image_surface, 0, 0)
            cr.paint()

        except Exception as e:
            log.error(f"Error in do_snapshot: {e}", exc_info=True)

    def do_get_intrinsic_width(self):
        """Return natural width."""
        return self._width

    def do_get_intrinsic_height(self):
        """Return natural height."""
        return self._height

    def do_get_flags(self):
        """Return paintable flags."""
        from gi.repository import Gdk

        return Gdk.PaintableFlags.CONTENTS  # Contents change over time


class CameraPaintable:
    """Custom GdkPaintable for AVFoundation camera frames.

    This class implements the GdkPaintable interface to display live camera
    frames in a Gtk.Picture widget. It mimics gtk4paintablesink behavior
    from Linux/GStreamer.
    """

    def __init__(self, picture_widget=None):
        """Initialize the paintable.

        Args:
            picture_widget: Optional Gtk.Picture to trigger redraws on
        """
        # Create instance of the GObject implementation
        self._impl = _CameraPaintableImpl(picture_widget)

    def update_frame(self, ci_image):
        """Update paintable with new frame."""
        self._impl.update_frame(ci_image)

    def get_paintable(self):
        """Get the GdkPaintable object."""
        return self._impl

    def set_picture_widget(self, picture_widget):
        """Set the Picture widget for triggering redraws."""
        self._impl._picture_widget = picture_widget

    def stop(self):
        """Stop processing frames."""
        self._impl.stop()


class QRScannerDelegate(NSObject):
    """Delegate for processing camera frames and detecting QR codes."""

    def init(self):
        """Initialize the delegate."""
        self = objc.super(QRScannerDelegate, self).init()
        if self is None:
            return None
        self._on_result = None
        self._session = None
        self._paintable = None
        self._last_preview_time = 0
        self._min_preview_interval = _CAMERA_FRAME_INTERVAL_S
        return self

    def setCallback_(self, callback):
        """Set the callback to call when a QR code is detected."""
        self._on_result = callback

    def setSession_(self, session):
        """Set the capture session to stop when QR code is found."""
        self._session = session

    def setPaintable_(self, paintable):
        """Set the paintable to update with camera frames."""
        self._paintable = paintable

    def captureOutput_didOutputSampleBuffer_fromConnection_(
        self, output, sample_buffer, connection
    ):
        """Process each camera frame to detect QR codes."""
        if self._on_result is None:
            return

        try:
            from CoreMedia import CMSampleBufferGetImageBuffer
            from Quartz import CIImage
            from Quartz.CoreVideo import (
                CVPixelBufferLockBaseAddress,
                CVPixelBufferUnlockBaseAddress,
            )

            # Get pixel buffer from sample buffer
            pixel_buffer = CMSampleBufferGetImageBuffer(sample_buffer)
            if pixel_buffer is None:
                return

            # Lock the base address
            CVPixelBufferLockBaseAddress(pixel_buffer, 0)

            try:
                # Process paintable update WHILE buffer is locked (before unlock)
                # This prevents race condition where buffer gets unlocked before processing
                if self._paintable:
                    # Throttle preview updates to ~30 fps
                    import time

                    current_time = time.time()
                    if current_time - self._last_preview_time >= self._min_preview_interval:
                        self._last_preview_time = current_time

                        # Process immediately on this thread while buffer is valid
                        pixel_data = self._paintable._impl._pixel_buffer_to_rgba(pixel_buffer)
                        if pixel_data:
                            # Schedule UI update on main thread with already-converted data
                            from gi.repository import GLib

                            data = pixel_data  # Capture in closure

                            def update_paintable():
                                if self._paintable and self._paintable._impl:
                                    self._paintable._impl._pixel_data = data
                                    self._paintable._impl._width = data["width"]
                                    self._paintable._impl._height = data["height"]
                                    self._paintable._impl._stride = data["stride"]
                                    self._paintable._impl.emit("invalidate-contents")
                                    if self._paintable._impl._picture_widget:
                                        self._paintable._impl._picture_widget.queue_draw()
                                return False

                            GLib.idle_add(update_paintable)

                # Create CIImage for QR detection
                ci_image = CIImage.imageWithCVPixelBuffer_(pixel_buffer)

                # Create Vision request handler
                handler = VNImageRequestHandler.alloc().initWithCIImage_options_(ci_image, None)

                # Create barcode detection request
                request = VNDetectBarcodesRequest.alloc().init()
                request.setSymbologies_(NSArray.arrayWithObject_(VNBarcodeSymbologyQR))

                # Perform the request
                success, error = handler.performRequests_error_(
                    NSArray.arrayWithObject_(request), None
                )

                if success:
                    # Get results
                    results = request.results()
                    if results and len(results) > 0:
                        # Get the first QR code found
                        barcode = results[0]
                        qr_data = barcode.payloadStringValue()

                        if qr_data:
                            log.info(f"QR code detected: {qr_data[:50]}...")

                            # Stop the session
                            if self._session:
                                self._session.stopRunning()

                            # Call the callback on main thread
                            if self._on_result:
                                from gi.repository import GLib

                                # Capture the callback before clearing
                                callback = self._on_result
                                self._on_result = None  # Clear immediately to prevent double-call

                                def callback_wrapper():
                                    try:
                                        return callback(qr_data) or False
                                    except Exception as e:
                                        log.error(f"QR callback failed: {e}", exc_info=True)
                                        return False

                                GLib.idle_add(callback_wrapper)

            finally:
                # Unlock the base address
                CVPixelBufferUnlockBaseAddress(pixel_buffer, 0)

        except Exception as e:
            log.error(f"Error processing camera frame: {e}", exc_info=True)


class QRScannerDialogMacOS:
    """QR code scanner dialog for macOS using AVFoundation."""

    def __init__(self, window, on_result):
        """Initialize the QR scanner dialog.

        Args:
            window: Parent GTK window
            on_result: Callback function(data: str) called with QR code data
        """
        self._window = window
        self._on_result = on_result
        self._session = None
        self._dialog = None
        self._delegate = None
        self._paintable = None  # CameraPaintable instance
        self._preview_image = None
        self._status_label = None
        self._scan_active = False
        self._hint_timer_id = None
        self._timeout_timer_id = None

    def start(self):
        """Start the QR scanner with camera preview."""
        if not AVFOUNDATION_AVAILABLE:
            log.error("AVFoundation not available - QR scanner disabled")
            self._window.show_toast("Camera not available. AVFoundation frameworks not installed.")
            return

        # Check camera authorization
        auth_status = AVCaptureDevice.authorizationStatusForMediaType_(AVMediaTypeVideo)

        if auth_status == AVAuthorizationStatusNotDetermined:
            # Request permission
            log.info("Requesting camera permission...")

            def completion_handler(granted):
                if granted:
                    log.info("Camera permission granted")
                    from gi.repository import GLib

                    GLib.idle_add(self._setup_camera)
                else:
                    log.warning("Camera permission denied")
                    from gi.repository import GLib

                    GLib.idle_add(
                        lambda: self._window.show_toast(
                            "Camera permission denied. Enable in System Settings > Privacy & Security > Camera"
                        )
                    )

            AVCaptureDevice.requestAccessForMediaType_completionHandler_(
                AVMediaTypeVideo, completion_handler
            )
            return

        elif auth_status == AVAuthorizationStatusAuthorized:
            self._setup_camera()
        else:
            log.warning("Camera access not authorized")
            self._window.show_toast(
                "Camera access denied. Enable in System Settings > Privacy & Security > Camera"
            )

    def _setup_camera(self):
        """Set up the camera capture session."""
        try:
            from gi.repository import Adw, Gtk

            # Create capture session
            self._session = AVCaptureSession.alloc().init()

            # Get default video device
            device = AVCaptureDevice.defaultDeviceWithMediaType_(AVMediaTypeVideo)
            if not device:
                log.error("No camera device found")
                self._window.show_toast("No camera found")
                return

            # Create device input
            device_input, error = AVCaptureDeviceInput.deviceInputWithDevice_error_(device, None)
            if not device_input or error:
                log.error(f"Failed to create device input: {error}")
                self._window.show_toast("Failed to access camera")
                return

            # Add input to session
            if self._session.canAddInput_(device_input):
                self._session.addInput_(device_input)
            else:
                log.error("Cannot add video input to session")
                self._window.show_toast("Failed to configure camera")
                return

            # Create video data output
            video_output = AVCaptureVideoDataOutput.alloc().init()

            # Note: We don't set videoSettings - AVFoundation will use a default compatible format
            # The Vision framework can handle various pixel formats for QR detection

            # Create custom paintable for camera preview
            self._paintable = CameraPaintable()

            # Create and set delegate
            self._delegate = QRScannerDelegate.alloc().init()
            self._delegate.setCallback_(self._on_qr_detected)
            self._delegate.setSession_(self._session)
            self._delegate.setPaintable_(self._paintable)

            # Create serial dispatch queue for frame processing
            queue = dispatch_queue_create(b"qr_scanner_queue", DISPATCH_QUEUE_SERIAL)
            video_output.setSampleBufferDelegate_queue_(self._delegate, queue)

            # Add output to session
            if self._session.canAddOutput_(video_output):
                self._session.addOutput_(video_output)
            else:
                log.error("Cannot add video output to session")
                self._window.show_toast("Failed to configure camera")
                return

            # Create dialog matching Linux UI
            self._dialog = Adw.Dialog()
            self._dialog.set_title("Scan QR Code")
            self._dialog.set_content_width(400)
            self._dialog.set_content_height(400)

            toolbar_view = Adw.ToolbarView()
            header = Adw.HeaderBar()
            toolbar_view.add_top_bar(header)

            content = Gtk.Box(
                orientation=Gtk.Orientation.VERTICAL,
                spacing=8,
                margin_start=8,
                margin_end=8,
                margin_bottom=8,
            )

            # Video preview (matching Linux dimensions)
            self._preview_image = Gtk.Picture()
            paintable_obj = self._paintable.get_paintable()
            self._preview_image.set_paintable(paintable_obj)

            # Tell paintable about Picture widget for queue_draw
            self._paintable.set_picture_widget(self._preview_image)

            self._preview_image.set_size_request(360, 270)
            self._preview_image.set_vexpand(True)
            self._preview_image.set_content_fit(Gtk.ContentFit.CONTAIN)
            content.append(self._preview_image)

            # Status label
            self._status_label = Gtk.Label(label="Point the camera at a QR code...")
            self._status_label.add_css_class("dim-label")
            content.append(self._status_label)

            toolbar_view.set_content(content)
            self._dialog.set_child(toolbar_view)

            # Handle dialog close
            self._dialog.connect("closed", lambda d: self._cleanup())

            # Start camera capture session
            self._session.startRunning()
            self._scan_active = True

            # Set up hint and timeout timers (matching Linux)
            from gi.repository import GLib

            self._hint_timer_id = GLib.timeout_add(_QR_SCAN_HINT_DELAY_MS, self._on_scan_hint)
            self._timeout_timer_id = GLib.timeout_add(_QR_SCAN_TIMEOUT_MS, self._on_scan_timeout)

            # Show dialog
            self._dialog.present(self._window)

        except Exception as e:
            log.error(f"Failed to set up camera: {e}", exc_info=True)
            self._window.show_toast(f"Camera error: {e}")
            self._cleanup()

    def _on_qr_detected(self, qr_data):
        """Called when a QR code is detected.

        Args:
            qr_data: The QR code data string
        """
        log.info(f"QR code scanned: {qr_data[:50]}...")

        self._scan_active = False

        # Stop paintable to prevent race conditions
        if self._paintable:
            self._paintable.stop()

        if self._status_label:
            self._status_label.set_label("QR code found!")

        # Close dialog
        if self._dialog:
            self._dialog.close()

        # Call the original callback
        if self._on_result:
            self._on_result(qr_data)

        # Cleanup
        self._cleanup()

    def _on_scan_hint(self):
        """Show a hint after 8 seconds of no detection."""
        self._hint_timer_id = None
        if self._scan_active and self._status_label:
            self._status_label.set_label(
                "No QR code detected yet.\n"
                "Make sure the code is well-lit, sharp, and fills most of the frame."
            )
        return False

    def _on_scan_timeout(self):
        """Show a give-up message after 30 seconds."""
        self._timeout_timer_id = None
        if self._scan_active and self._status_label:
            self._status_label.set_label(
                "Could not detect QR code.\n" "Try importing the contact via clipboard instead."
            )
        return False

    def _cleanup(self):
        """Clean up camera resources."""
        self._scan_active = False

        # Stop paintable to prevent frame processing
        if self._paintable:
            self._paintable.stop()

        # Remove timers
        if self._hint_timer_id:
            from gi.repository import GLib

            GLib.source_remove(self._hint_timer_id)
            self._hint_timer_id = None
        if self._timeout_timer_id:
            from gi.repository import GLib

            GLib.source_remove(self._timeout_timer_id)
            self._timeout_timer_id = None

        # Stop camera session
        if self._session and self._session.isRunning():
            self._session.stopRunning()

        self._session = None
        self._delegate = None
        self._paintable = None
        self._dialog = None
        self._preview_image = None
        self._status_label = None
