use super::{Capture, CaptureError, CaptureEvent, Position, error::MacosCaptureCreationError};
use async_trait::async_trait;
use bitflags::bitflags;
use core_foundation::{
    base::{CFRelease, TCFType, kCFAllocatorDefault},
    date::CFTimeInterval,
    number::{CFBooleanRef, kCFBooleanTrue},
    runloop::{CFRunLoop, CFRunLoopSource, CFRunLoopSourceRef, kCFRunLoopCommonModes},
    string::{CFStringCreateWithCString, CFStringRef, kCFStringEncodingUTF8},
};
use core_graphics::{
    base::{CGError, CGFloat, kCGErrorSuccess},
    display::{CGDisplay, CGPoint},
    event::{
        CGEvent, CGEventFlags, CGEventTap, CGEventTapLocation, CGEventTapOptions,
        CGEventTapPlacement, CGEventTapProxy, CGEventType, CallbackResult, EventField,
    },
    event_source::{CGEventSource, CGEventSourceStateID},
};
use futures_core::Stream;
use input_event::{
    BTN_BACK, BTN_FORWARD, BTN_LEFT, BTN_MIDDLE, BTN_RIGHT, Event, KeyboardEvent, PointerEvent,
};
use keycode::{KeyMap, KeyMapping};
use libc::c_void;
use once_cell::unsync::Lazy;
use std::{
    collections::HashSet,
    ffi::{CString, c_char},
    pin::Pin,
    sync::{Arc, OnceLock},
    task::{Context, Poll, ready},
    thread::{self},
};
use tokio::sync::{
    Mutex,
    mpsc::{self, Receiver, Sender},
    oneshot,
};

#[derive(Debug, Default)]
struct Bounds {
    xmin: f64,
    xmax: f64,
    ymin: f64,
    ymax: f64,
}

#[derive(Debug)]
struct InputCaptureState {
    /// active capture positions
    active_clients: Lazy<HashSet<Position>>,
    /// the currently entered capture position, if any
    current_pos: Option<Position>,
    /// position where the cursor was captured
    enter_position: Option<CGPoint>,
    /// bounds of the input capture area
    bounds: Bounds,
    /// current state of modifier keys
    modifier_state: XMods,
}

#[derive(Debug)]
enum ProducerEvent {
    /// `warp_target`, when present, is a screen-space (Quartz) point
    /// at which to warp the local cursor before showing it. Used to
    /// preserve cross-axis continuity on release: the visible cursor
    /// reappears at the host point matching where it visually was on
    /// the guest, instead of snapping back to the capture-start edge.
    Release {
        warp_target: Option<(i32, i32)>,
    },
    Create(Position),
    Destroy(Position),
    Grab(Position),
    EventTapDisabled,
    DisplayReconfigured,
}

impl InputCaptureState {
    fn new() -> Result<Self, MacosCaptureCreationError> {
        let mut res = Self {
            active_clients: Lazy::new(HashSet::new),
            current_pos: None,
            enter_position: None,
            bounds: Bounds::default(),
            modifier_state: Default::default(),
        };
        res.update_bounds()?;
        Ok(res)
    }

    fn crossed(&mut self, event: &CGEvent) -> Option<Position> {
        let location = event.location();
        let relative_x = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X);
        let relative_y = event.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y);

        for &position in self.active_clients.iter() {
            if (position == Position::Left && (location.x + relative_x) <= self.bounds.xmin)
                || (position == Position::Right && (location.x + relative_x) >= self.bounds.xmax)
                || (position == Position::Top && (location.y + relative_y) <= self.bounds.ymin)
                || (position == Position::Bottom && (location.y + relative_y) >= self.bounds.ymax)
            {
                log::debug!("Crossed barrier into position: {position:?}");
                return Some(position);
            }
        }
        None
    }

    // Get the max bounds of all displays
    fn update_bounds(&mut self) -> Result<(), MacosCaptureCreationError> {
        let active_ids =
            CGDisplay::active_displays().map_err(MacosCaptureCreationError::ActiveDisplays)?;
        active_ids.iter().for_each(|d| {
            let bounds = CGDisplay::new(*d).bounds();
            self.bounds.xmin = self.bounds.xmin.min(bounds.origin.x);
            self.bounds.xmax = self.bounds.xmax.max(bounds.origin.x + bounds.size.width);
            self.bounds.ymin = self.bounds.ymin.min(bounds.origin.y);
            self.bounds.ymax = self.bounds.ymax.max(bounds.origin.y + bounds.size.height);
        });

        log::debug!("Updated displays bounds: {0:?}", self.bounds);
        Ok(())
    }

    /// start the input capture by
    fn start_capture(&mut self, event: &CGEvent, position: Position) -> Result<(), CaptureError> {
        let mut location = event.location();
        let edge_offset = 1.0;
        // move cursor location to display bounds
        match position {
            Position::Left => location.x = self.bounds.xmin + edge_offset,
            Position::Right => location.x = self.bounds.xmax - edge_offset,
            Position::Top => location.y = self.bounds.ymin + edge_offset,
            Position::Bottom => location.y = self.bounds.ymax - edge_offset,
        };
        self.enter_position = Some(location);
        self.reset_cursor()
    }

    /// resets the cursor to the position, where the capture started
    fn reset_cursor(&mut self) -> Result<(), CaptureError> {
        let pos = self.enter_position.expect("capture active");
        log::trace!("Resetting cursor position to: {}, {}", pos.x, pos.y);
        CGDisplay::warp_mouse_cursor_position(pos).map_err(CaptureError::WarpCursor)
    }

    fn hide_cursor(&self) -> Result<(), CaptureError> {
        CGDisplay::hide_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
    }

    fn show_cursor(&self) -> Result<(), CaptureError> {
        CGDisplay::show_cursor(&CGDisplay::main()).map_err(CaptureError::CoreGraphics)
    }

    async fn handle_producer_event(
        &mut self,
        producer_event: ProducerEvent,
    ) -> Result<(), CaptureError> {
        log::debug!("handling event: {producer_event:?}");
        match producer_event {
            ProducerEvent::Release { warp_target } => {
                log::info!(
                    "[release-warp] handle_producer_event Release: current_pos={:?} warp_target={warp_target:?}",
                    self.current_pos
                );
                if self.current_pos.is_some() {
                    // Warp BEFORE clearing current_pos so the
                    // event-tap callback can't see Some(pos) and
                    // re-snap the cursor to the entry edge before we
                    // make it visible again. Then show_cursor() reveals
                    // it at the warped point.
                    if let Some((x, y)) = warp_target {
                        log::info!("[release-warp] warping local cursor to ({x}, {y})");
                        if let Err(e) = CGDisplay::warp_mouse_cursor_position(CGPoint {
                            x: x as CGFloat,
                            y: y as CGFloat,
                        }) {
                            log::warn!("[release-warp] warp_mouse_cursor_position failed: {e:?}");
                        }
                    }
                    self.show_cursor()?;
                    self.current_pos = None;
                }
            }
            ProducerEvent::Grab(pos) => {
                if self.current_pos.is_none() {
                    self.hide_cursor()?;
                    self.current_pos = Some(pos);
                }
            }
            ProducerEvent::Create(p) => {
                self.active_clients.insert(p);
            }
            ProducerEvent::Destroy(p) => {
                if let Some(current) = self.current_pos {
                    if current == p {
                        self.show_cursor()?;
                        self.current_pos = None;
                    };
                }
                self.active_clients.remove(&p);
            }
            ProducerEvent::EventTapDisabled => {
                // Tap death can happen mid-capture (TCC Accessibility
                // revoked, tap-timeout, etc). Release state so we
                // don't leave the cursor hidden even if the outer
                // task only logs this error rather than propagating.
                if self.current_pos.is_some() {
                    self.show_cursor()?;
                    self.current_pos = None;
                }
                // A tap disabled while the host screen is locked is
                // the lock screen's secure-input mode, not a
                // permission revocation. AXIsProcessTrusted() is
                // unreliable in the locked loginwindow session and
                // can read `false` with the grant fully intact —
                // probing it here would exit the daemon on every
                // lock (and screen-saver). Treat lock as recoverable:
                // the `com.apple.screenIsUnlocked` observer on the
                // event-tap thread re-enables the tap once the user
                // logs back in. Bail out before the AX probe.
                if is_screen_locked() {
                    log::info!(
                        "CGEventTap disabled while screen locked — will re-enable on unlock"
                    );
                    return Ok(());
                }
                // Distinguish AX revocation from a recoverable cause
                // (secure-input mode while typing in a password field
                // also fires TapDisabledByUserInput). If AX is gone,
                // the tap can't be recreated and the GUI's polling
                // watcher may not flip for a while when the user
                // *removed* the entry from System Settings → Privacy
                // & Security → Accessibility (vs just toggling it
                // off — removal can leave AXIsProcessTrusted reporting
                // cached-true in already-running processes). Exit
                // the daemon process directly: the GUI will see its
                // IPC connection drop and trigger its own
                // quit-with-backstop path. This is the only reliable
                // way to tear down a wedged HID-level tap quickly.
                if !unsafe { AXIsProcessTrusted() } {
                    log::error!(
                        "CGEventTap disabled and Accessibility no longer granted — daemon exiting"
                    );
                    std::process::exit(0);
                }
                return Err(CaptureError::EventTapDisabled);
            }
            ProducerEvent::DisplayReconfigured => {
                // The macOS display configuration changed — a monitor
                // was plugged in/out, the resolution changed, the
                // arrangement was rearranged, etc. Re-fetch the
                // active-display bounds so barrier crossings and the
                // cursor-warp on capture-start use the current
                // geometry instead of whatever was true at process
                // start.
                if let Err(e) = self.update_bounds() {
                    log::warn!("failed to refresh display bounds: {e}");
                } else {
                    log::info!("display reconfigured: {:?}", self.bounds);
                }
            }
        };
        Ok(())
    }
}

fn get_events(
    ev_type: &CGEventType,
    ev: &CGEvent,
    result: &mut Vec<CaptureEvent>,
    modifier_state: &mut XMods,
) -> Result<(), CaptureError> {
    fn map_pointer_event(ev: &CGEvent) -> PointerEvent {
        PointerEvent::Motion {
            time: 0,
            dx: ev.get_double_value_field(EventField::MOUSE_EVENT_DELTA_X),
            dy: ev.get_double_value_field(EventField::MOUSE_EVENT_DELTA_Y),
        }
    }

    fn map_key(ev: &CGEvent) -> Result<u32, CaptureError> {
        let code = ev.get_integer_value_field(EventField::KEYBOARD_EVENT_KEYCODE);
        match KeyMap::from_key_mapping(KeyMapping::Mac(code as u16)) {
            Ok(k) => Ok(k.evdev as u32),
            Err(()) => Err(CaptureError::KeyMapError(code)),
        }
    }

    match ev_type {
        CGEventType::KeyDown => {
            // Drop OS-generated auto-repeat KeyDowns. macOS streams a
            // run of KeyDown events (this field set to 1) while a key is
            // held; forwarding them would collide with the sink's own
            // repeat generation — every sink (Linux compositors, and the
            // macOS/Windows repeat tasks) synthesizes repeat from a
            // single held key. Forward only the genuine initial press so
            // the sink owns repeat timing.
            if ev.get_integer_value_field(EventField::KEYBOARD_EVENT_AUTOREPEAT) != 0 {
                return Ok(());
            }
            let k = map_key(ev)?;
            result.push(CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
                time: 0,
                key: k,
                state: 1,
            })));
        }
        CGEventType::KeyUp => {
            let k = map_key(ev)?;
            result.push(CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
                time: 0,
                key: k,
                state: 0,
            })));
        }
        CGEventType::FlagsChanged => {
            let mut depressed = XMods::empty();
            let mut mods_locked = XMods::empty();
            let cg_flags = ev.get_flags();

            if cg_flags.contains(CGEventFlags::CGEventFlagShift) {
                depressed |= XMods::ShiftMask;
            }
            if cg_flags.contains(CGEventFlags::CGEventFlagControl) {
                depressed |= XMods::ControlMask;
            }
            if cg_flags.contains(CGEventFlags::CGEventFlagAlternate) {
                depressed |= XMods::Mod1Mask;
            }
            if cg_flags.contains(CGEventFlags::CGEventFlagCommand) {
                depressed |= XMods::Mod4Mask;
            }
            if cg_flags.contains(CGEventFlags::CGEventFlagAlphaShift) {
                depressed |= XMods::LockMask;
                mods_locked |= XMods::LockMask;
            }

            // check if pressed or released
            let state = if depressed > *modifier_state { 1 } else { 0 };
            *modifier_state = depressed;

            if let Ok(key) = map_key(ev) {
                let key_event = CaptureEvent::Input(Event::Keyboard(KeyboardEvent::Key {
                    time: 0,
                    key,
                    state,
                }));
                result.push(key_event);
            }

            let modifier_event = KeyboardEvent::Modifiers {
                depressed: depressed.bits(),
                latched: 0,
                locked: mods_locked.bits(),
                group: 0,
            };

            result.push(CaptureEvent::Input(Event::Keyboard(modifier_event)));
        }
        CGEventType::MouseMoved => {
            result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
        }
        CGEventType::LeftMouseDragged => {
            result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
        }
        CGEventType::RightMouseDragged => {
            result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
        }
        CGEventType::OtherMouseDragged => {
            result.push(CaptureEvent::Input(Event::Pointer(map_pointer_event(ev))))
        }
        CGEventType::LeftMouseDown => {
            result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
                time: 0,
                button: BTN_LEFT,
                state: 1,
            })))
        }
        CGEventType::LeftMouseUp => {
            result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
                time: 0,
                button: BTN_LEFT,
                state: 0,
            })))
        }
        CGEventType::RightMouseDown => {
            result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
                time: 0,
                button: BTN_RIGHT,
                state: 1,
            })))
        }
        CGEventType::RightMouseUp => {
            result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
                time: 0,
                button: BTN_RIGHT,
                state: 0,
            })))
        }
        CGEventType::OtherMouseDown => {
            let btn_num = ev.get_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER);
            let button = match btn_num {
                3 => BTN_BACK,
                4 => BTN_FORWARD,
                _ => BTN_MIDDLE,
            };
            result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
                time: 0,
                button,
                state: 1,
            })))
        }
        CGEventType::OtherMouseUp => {
            let btn_num = ev.get_integer_value_field(EventField::MOUSE_EVENT_BUTTON_NUMBER);
            let button = match btn_num {
                3 => BTN_BACK,
                4 => BTN_FORWARD,
                _ => BTN_MIDDLE,
            };
            result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Button {
                time: 0,
                button,
                state: 0,
            })))
        }
        CGEventType::ScrollWheel => {
            // Emit scroll deltas in the *classic* mouse-wheel convention
            // (the historical baseline that predates natural scrolling)
            // regardless of the user's macOS Natural Scrolling
            // preference. Rationale:
            //
            //   1. Classic was the canonical scroll convention when
            //      the scroll wheel was invented; using it as the
            //      wire format keeps Mousehop predictable for any
            //      receiver, including non-natural-aware peers.
            //   2. Receivers opt into natural-feel via their own
            //      `natural_scroll` config, mirroring how libinput's
            //      natural_scroll knob works for physical input.
            //   3. macOS Natural Scrolling pre-flips POINT_DELTA at
            //      the OS layer; CGEventTap at Session placement sees
            //      events after that flip. So:
            //        Natural ON: POINT_DELTA already flipped (away
            //          from classic) → re-flip back to classic by
            //          NOT flipping in our code (sign = +1).
            //        Natural OFF: POINT_DELTA already in classic →
            //          flip once to invert away from raw and… wait,
            //          actually we want to land on classic regardless.
            //          With Natural OFF the OS gives us "raw classic"
            //          *as-the-mac-sees-it*; our peers' wl_pointer
            //          treats positive Y as "document moves down on
            //          screen" (natural-feel). To present classic
            //          feel on the wire we negate (sign = -1).
            //
            // Net result: wire is consistently classic-feel regardless
            // of the Mac's preference. Receivers can re-invert.
            let sign: i64 = if natural_scrolling_enabled() { 1 } else { -1 };
            if ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_IS_CONTINUOUS) != 0 {
                let v = sign
                    * ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_1);
                let h = sign
                    * ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_POINT_DELTA_AXIS_2);
                if v != 0 {
                    result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
                        time: 0,
                        axis: 0, // Vertical
                        value: v as f64,
                    })));
                }
                if h != 0 {
                    result.push(CaptureEvent::Input(Event::Pointer(PointerEvent::Axis {
                        time: 0,
                        axis: 1, // Horizontal
                        value: h as f64,
                    })));
                }
            } else {
                // line based scrolling
                //
                // macOS already amplifies SCROLL_WHEEL_EVENT_DELTA based
                // on wheel velocity — a slow notch on a notched wheel
                // (e.g. MX Master 4) reports DELTA=1, a fast flick
                // reports DELTA=10+ per event. The wl_pointer v120
                // protocol expects one physical wheel click = 120
                // units, so map one macOS DELTA line to one full v120
                // tick. (The previous 3-lines-per-step ratio caused
                // single notches to truncate to discrete=0 on the
                // receiver, leaving Slack/Alacritty unscrollable until
                // 3+ notches accumulated.)
                const LINES_PER_STEP: i32 = 1;
                const V120_STEPS_PER_LINE: i32 = 120 / LINES_PER_STEP;
                let v =
                    sign * ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_1);
                let h =
                    sign * ev.get_integer_value_field(EventField::SCROLL_WHEEL_EVENT_DELTA_AXIS_2);
                if v != 0 {
                    result.push(CaptureEvent::Input(Event::Pointer(
                        PointerEvent::AxisDiscrete120 {
                            axis: 0, // Vertical
                            value: V120_STEPS_PER_LINE * v as i32,
                        },
                    )));
                }
                if h != 0 {
                    result.push(CaptureEvent::Input(Event::Pointer(
                        PointerEvent::AxisDiscrete120 {
                            axis: 1, // Horizontal
                            value: V120_STEPS_PER_LINE * h as i32,
                        },
                    )));
                }
            }
        }
        _ => (),
    }
    Ok(())
}

fn create_event_tap<'a>(
    client_state: Arc<Mutex<InputCaptureState>>,
    notify_tx: Sender<ProducerEvent>,
    event_tx: Sender<(Position, CaptureEvent)>,
) -> Result<(CGEventTap<'a>, Arc<OnceLock<usize>>), MacosCaptureCreationError> {
    // Shared slot for the tap's mach port pointer. Stored as `usize`
    // because raw pointers aren't `Send`, but the integer
    // representation is — and CGEventTapEnable is documented as
    // thread-safe. Set immediately after CGEventTap::new returns;
    // read by the callback to recover from a TapDisabledByTimeout.
    let tap_mach_port: Arc<OnceLock<usize>> = Arc::new(OnceLock::new());
    let tap_mach_port_cb = Arc::clone(&tap_mach_port);

    let cg_events_of_interest: Vec<CGEventType> = vec![
        CGEventType::LeftMouseDown,
        CGEventType::LeftMouseUp,
        CGEventType::RightMouseDown,
        CGEventType::RightMouseUp,
        CGEventType::OtherMouseDown,
        CGEventType::OtherMouseUp,
        CGEventType::MouseMoved,
        CGEventType::LeftMouseDragged,
        CGEventType::RightMouseDragged,
        CGEventType::OtherMouseDragged,
        CGEventType::ScrollWheel,
        CGEventType::KeyDown,
        CGEventType::KeyUp,
        CGEventType::FlagsChanged,
    ];

    let event_tap_callback = move |_proxy: CGEventTapProxy,
                                   event_type: CGEventType,
                                   cg_ev: &CGEvent| {
        log::trace!("Got event from tap: {event_type:?}");
        let mut state = client_state.blocking_lock();
        let mut capture_position = None;
        let mut res_events = vec![];

        if matches!(event_type, CGEventType::TapDisabledByTimeout) {
            // The kernel disables the tap when our callback runs
            // longer than ~1s on a single event — typical causes
            // are heavy load, scheduler contention, or this
            // process being briefly suspended (e.g. App Nap on a
            // long idle). It is NOT a fatal condition: Apple's
            // documented recovery is to call CGEventTapEnable
            // and resume processing. Re-enable in place and KEEP
            // existing capture state so the user doesn't see the
            // cursor pop back to the local screen mid-session.
            if let Some(&port) = tap_mach_port_cb.get() {
                log::warn!("CGEventTap disabled by timeout — re-enabling");
                unsafe {
                    CGEventTapEnable(port as *mut c_void, true);
                }
            } else {
                log::error!(
                    "CGEventTap disabled by timeout, but mach port not yet stored — cannot re-enable"
                );
            }
            return CallbackResult::Keep;
        }

        if matches!(event_type, CGEventType::TapDisabledByUserInput) {
            // Deliberate kill — secure-input mode (e.g. password
            // field), TCC Accessibility revoked mid-session, or
            // the user disabling event-monitoring. We can't
            // recover from this; drop captured state synchronously
            // and return Keep on this event. Otherwise the
            // `current_pos.is_some()` branch below would drop this
            // event (and any racing callback still in flight) back
            // into `CallbackResult::Drop`, silently eating the
            // user's clicks and keypresses while the tap winds
            // down. Clear state + show the cursor here, then
            // notify the producer loop so the service can tear
            // down cleanly.
            log::error!("CGEventTap disabled by user input, releasing capture state");
            if state.current_pos.is_some() {
                let _ = CGDisplay::show_cursor(&CGDisplay::main());
                state.current_pos = None;
            }
            notify_tx
                .blocking_send(ProducerEvent::EventTapDisabled)
                .unwrap_or_else(|e| {
                    log::error!("Failed to send notification: {e}");
                });
            return CallbackResult::Keep;
        }

        // Are we in a client?
        if let Some(current_pos) = state.current_pos {
            capture_position = Some(current_pos);
            get_events(
                &event_type,
                cg_ev,
                &mut res_events,
                &mut state.modifier_state,
            )
            .unwrap_or_else(|e| {
                log::error!("Failed to get events: {e}");
            });

            // Keep (hidden) cursor at the edge of the screen
            if matches!(
                event_type,
                CGEventType::MouseMoved
                    | CGEventType::LeftMouseDragged
                    | CGEventType::RightMouseDragged
                    | CGEventType::OtherMouseDragged
            ) {
                state.reset_cursor().unwrap_or_else(|e| log::warn!("{e}"));
            }
        } else if matches!(event_type, CGEventType::MouseMoved) {
            // Did we cross a barrier?
            if let Some(new_pos) = state.crossed(cg_ev) {
                // About to commit the cross — final gate: skip if the
                // host is locked, since the lock screen consumes
                // keyboard before our tap sees it and allowing the
                // cursor to leave would produce a mouse-only-on-peer
                // half-broken state. Polling CGSession only at this
                // commit point (rather than every MouseMoved) keeps
                // the per-event cost zero — `is_screen_locked()` is
                // an XPC to WindowServer (~10–50µs); a typical user
                // crosses a wall a few times per minute.
                if is_screen_locked() {
                    log::info!("host screen locked; suppressing cross to {new_pos:?}");
                } else {
                    capture_position = Some(new_pos);
                    // Snapshot the cursor's screen-space position at the
                    // instant of crossing — before start_capture's
                    // reset_cursor() snaps it to the edge. The peer uses
                    // this for the visually-corresponding warp on Enter
                    // so the cursor doesn't jump to the entry-edge midpoint.
                    let cross_loc = cg_ev.location();
                    let cursor = Some((cross_loc.x as i32, cross_loc.y as i32));
                    state
                        .start_capture(cg_ev, new_pos)
                        .unwrap_or_else(|e| log::warn!("{e}"));
                    res_events.push(CaptureEvent::Begin { cursor });
                    notify_tx
                        .blocking_send(ProducerEvent::Grab(new_pos))
                        .expect("Failed to send notification");
                }
            }
        }

        if let Some(pos) = capture_position {
            for e in res_events {
                // This callback runs on the kernel's event-delivery
                // thread: time spent here delays input for the whole
                // system, and a *blocked* callback freezes input
                // outright until kCGEventTapDisabledByTimeout fires —
                // the user sees laggy, dropped keystrokes. So never
                // block unconditionally on a backed-up forwarding
                // channel (a slow/congested peer link fills it).
                //
                // Pointer-motion is a delta stream: a dropped sample
                // is absorbed by the next one, so under pressure we
                // drop it rather than block. Keyboard, button, scroll
                // and lifecycle (Begin/AutoRelease) events are NOT
                // self-correcting — a dropped key-up sticks a key on
                // the peer — so for those, and only those, we accept
                // a brief block. They're low-volume, so the channel
                // is realistically only ever full of motion samples,
                // which the drop path drains.
                match event_tx.try_send((pos, e)) {
                    Ok(()) => {}
                    Err(mpsc::error::TrySendError::Full((pos, e))) => {
                        if matches!(
                            e,
                            CaptureEvent::Input(Event::Pointer(PointerEvent::Motion { .. }))
                        ) {
                            log::debug!("forwarding channel full — dropping pointer-motion sample");
                        } else {
                            log::warn!("forwarding channel full — blocking to preserve {e}");
                            // Closed only happens on shutdown; ignore.
                            let _ = event_tx.blocking_send((pos, e));
                        }
                    }
                    // Channel closed: the InputCapture instance is
                    // being dropped. Nothing to forward to; ignore.
                    Err(mpsc::error::TrySendError::Closed(_)) => {}
                }
            }
            // Returning Drop should stop the event from being processed
            // but core fundation still returns the event
            cg_ev.set_type(CGEventType::Null);
            CallbackResult::Drop
        } else {
            CallbackResult::Keep
        }
    };

    let tap = CGEventTap::new(
        CGEventTapLocation::Session,
        CGEventTapPlacement::HeadInsertEventTap,
        CGEventTapOptions::Default,
        cg_events_of_interest,
        event_tap_callback,
    )
    .map_err(|_| MacosCaptureCreationError::EventTapCreation)?;

    // Hand the mach port pointer to the callback so it can re-enable
    // the tap on TapDisabledByTimeout. The pointer is valid for the
    // lifetime of `tap` (which lives on the event-tap thread until
    // the run loop exits).
    let port_ptr = tap.mach_port().as_concrete_TypeRef() as usize;
    let _ = tap_mach_port.set(port_ptr);

    // Hand the same slot back to the caller so the screen-unlock
    // observer can re-enable the tap after a lock disables it.

    let tap_source: CFRunLoopSource = tap
        .mach_port()
        .create_runloop_source(0)
        .expect("Failed creating loop source");

    unsafe {
        CFRunLoop::get_current().add_source(&tap_source, kCFRunLoopCommonModes);
    }

    Ok((tap, tap_mach_port))
}

fn event_tap_thread(
    client_state: Arc<Mutex<InputCaptureState>>,
    event_tx: Sender<(Position, CaptureEvent)>,
    notify_tx: Sender<ProducerEvent>,
    ready: std::sync::mpsc::Sender<Result<CFRunLoop, MacosCaptureCreationError>>,
    exit: oneshot::Sender<()>,
) {
    // Clone now: create_event_tap consumes notify_tx into its closure.
    let display_notify_tx = notify_tx.clone();

    let (_tap, tap_mach_port) = match create_event_tap(client_state, notify_tx, event_tx) {
        Err(e) => {
            ready.send(Err(e)).expect("channel closed");
            return;
        }
        Ok((tap, port)) => {
            let run_loop = CFRunLoop::get_current();
            ready.send(Ok(run_loop)).expect("channel closed");
            (tap, port)
        }
    };

    // Subscribe to the screen-unlock distributed notification. When
    // the host locks, the lock screen's password field enables
    // secure event input, which disables our session event tap.
    // macOS never re-enables it on unlock — without this observer
    // the daemon survives the lock but the tap stays dead and
    // mousehop silently stops capturing. Box-leak the refcon so the
    // C side has a stable observer pointer; reclaim it after the run
    // loop exits.
    let unlock_ctx = Box::into_raw(Box::new(UnlockObserverCtx {
        tap_mach_port: Arc::clone(&tap_mach_port),
    }));
    let unlock_name = unsafe {
        let cstr = CString::new("com.apple.screenIsUnlocked").unwrap();
        CFStringCreateWithCString(
            kCFAllocatorDefault,
            cstr.as_ptr() as *const c_char,
            kCFStringEncodingUTF8,
        )
    };
    unsafe {
        // CFNotificationSuspensionBehaviorDeliverImmediately — the
        // observer's thread run loop is always running here, but
        // deliver-immediately also covers the brief window around a
        // lock/unlock transition.
        const DELIVER_IMMEDIATELY: isize = 4;
        CFNotificationCenterAddObserver(
            CFNotificationCenterGetDistributedCenter(),
            unlock_ctx as *const c_void,
            screen_unlocked_callback,
            unlock_name,
            std::ptr::null(),
            DELIVER_IMMEDIATELY,
        );
    }

    // Register a Quartz display-reconfiguration callback so the
    // capture state's bounds get refreshed when the user plugs in a
    // monitor, changes resolution, or rearranges displays. The
    // callback runs on this thread's CFRunLoop. Box-leak the sender
    // so the C side has a stable user_info pointer; reclaim it after
    // the run loop exits.
    let display_user_info = Box::into_raw(Box::new(display_notify_tx.clone())) as *mut c_void;
    unsafe {
        CGDisplayRegisterReconfigurationCallback(
            display_reconfiguration_callback,
            display_user_info,
        );
    }

    // Also subscribe to system-power events so we recover from
    // sleep/wake, where the Quartz reconfigure callback may not
    // fire (or fires before our run loop is processing again, e.g.
    // clamshell-disconnect → lid-open). On wake we send the same
    // DisplayReconfigured event the existing handler consumes, so
    // bounds get refreshed for free.
    let mut power_notifier_object: u32 = 0;
    let mut power_notification_port: *mut c_void = std::ptr::null_mut();
    let power_ctx = Box::into_raw(Box::new(PowerCtx {
        sender: display_notify_tx,
        root_port: 0,
    }));
    let power_root_port = unsafe {
        let port = IORegisterForSystemPower(
            power_ctx as *mut c_void,
            &mut power_notification_port,
            power_callback,
            &mut power_notifier_object,
        );
        // Stash the root port for the callback's IOAllowPowerChange
        // ack — we couldn't know it at Box-construction time because
        // it's the registration's return value.
        (*power_ctx).root_port = port;
        if !power_notification_port.is_null() {
            let src_ref = IONotificationPortGetRunLoopSource(power_notification_port);
            if !src_ref.is_null() {
                let src = CFRunLoopSource::wrap_under_get_rule(src_ref);
                CFRunLoop::get_current().add_source(&src, kCFRunLoopCommonModes);
            }
        }
        port
    };

    log::debug!("running CFRunLoop...");
    CFRunLoop::run_current();
    log::debug!("event tap thread exiting!...");

    unsafe {
        CGDisplayRemoveReconfigurationCallback(display_reconfiguration_callback, display_user_info);
        // Reclaim the leaked sender Box so we don't leak a tokio
        // channel sender on every capture create/destroy cycle.
        drop(Box::from_raw(
            display_user_info as *mut Sender<ProducerEvent>,
        ));

        // Tear down the screen-unlock observer and reclaim its
        // refcon Box, mirroring the display-callback cleanup above.
        CFNotificationCenterRemoveEveryObserver(
            CFNotificationCenterGetDistributedCenter(),
            unlock_ctx as *const c_void,
        );
        drop(Box::from_raw(unlock_ctx));
        CFRelease(unlock_name as *const c_void);

        if power_notifier_object != 0 {
            let _ = IODeregisterForSystemPower(&mut power_notifier_object);
        }
        if !power_notification_port.is_null() {
            IONotificationPortDestroy(power_notification_port);
        }
        let _ = power_root_port;
        drop(Box::from_raw(power_ctx));
    }

    let _ = exit.send(());
}

/// Query whether the host's screen is locked. Asks the WindowServer
/// for the current login session dictionary and looks up the
/// `CGSSessionScreenIsLocked` key. The key is `kCFBooleanTrue` when
/// locked; on Sequoia 15+ it's typically absent when unlocked rather
/// than `kCFBooleanFalse`, so missing-or-nil is treated as unlocked.
/// Costs ~10–50µs per call (an XPC round-trip to WindowServer);
/// called from the event tap callback only on `MouseMoved`, so the
/// amortized cost is negligible (<2% CPU at typical mouse rates).
fn is_screen_locked() -> bool {
    let key = unsafe {
        let cstr = CString::new("CGSSessionScreenIsLocked").unwrap();
        CFStringCreateWithCString(
            kCFAllocatorDefault,
            cstr.as_ptr() as *const c_char,
            kCFStringEncodingUTF8,
        )
    };
    let dict = unsafe { CGSessionCopyCurrentDictionary() };
    if dict.is_null() {
        unsafe { CFRelease(key as *const c_void) };
        return false;
    }
    let value = unsafe { CFDictionaryGetValue(dict, key as *const c_void) };
    let locked = !value.is_null() && unsafe { CFBooleanGetValue(value as CFBooleanRef) };
    unsafe {
        CFRelease(dict as *const c_void);
        CFRelease(key as *const c_void);
    }
    locked
}

/// Refcon for the IOKit system-power callback. Bundles the channel
/// sender (so the callback can post `DisplayReconfigured` on wake)
/// and the `io_connect_t` root port (so the callback can ack
/// sleep-related messages with `IOAllowPowerChange`). Built on the
/// event-tap thread, used only by the callback on the same thread —
/// never crosses thread boundaries, so no Send/Sync needed.
struct PowerCtx {
    sender: Sender<ProducerEvent>,
    root_port: u32,
}

/// IOKit system-power callback. Fires for every power-management
/// transition (CanSleep, WillSleep, WillPowerOn, HasPoweredOn).
/// We only care about `kIOMessageSystemHasPoweredOn` (post-wake);
/// for the sleep-pending messages we just ack so the kernel doesn't
/// hold the system in its "waiting for clients" state for the full
/// 30-second timeout.
extern "C" fn power_callback(
    refcon: *mut c_void,
    _service: u32,
    msg_type: u32,
    msg_arg: *mut c_void,
) {
    const K_IO_MESSAGE_CAN_SYSTEM_SLEEP: u32 = 0xE000_0270;
    const K_IO_MESSAGE_SYSTEM_WILL_SLEEP: u32 = 0xE000_0280;
    const K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON: u32 = 0xE000_0300;

    if refcon.is_null() {
        return;
    }
    // SAFETY: `refcon` is `Box::into_raw(Box::new(PowerCtx))` owned by
    // `event_tap_thread`; valid until the run loop exits and the box
    // is reclaimed. The callback only fires while the run loop runs
    // on that thread, so the box is live here.
    let ctx = unsafe { &*(refcon as *const PowerCtx) };
    match msg_type {
        K_IO_MESSAGE_CAN_SYSTEM_SLEEP | K_IO_MESSAGE_SYSTEM_WILL_SLEEP => {
            // Ack so the OS doesn't stall on its 30s default timeout.
            // `msg_arg` carries the notification ID (an `intptr_t`);
            // pass it through verbatim.
            unsafe {
                IOAllowPowerChange(ctx.root_port, msg_arg as isize);
            }
        }
        K_IO_MESSAGE_SYSTEM_HAS_POWERED_ON => {
            // Bounce a DisplayReconfigured into the producer so
            // `update_bounds()` runs. Covers the case where Quartz's
            // own reconfigure callback didn't fire (or fired during
            // the sleep window) — e.g. clamshell-disconnect →
            // lid-open transitions.
            log::info!("system woke from sleep; refreshing display bounds");
            if let Err(e) = ctx.sender.blocking_send(ProducerEvent::DisplayReconfigured) {
                log::warn!("failed to post wake → DisplayReconfigured: {e}");
            }
        }
        _ => {}
    }
}

/// Quartz display-reconfiguration callback. Fires twice per change:
/// once with `kCGDisplayBeginConfigurationFlag` set (BEFORE the
/// change is applied — the bounds are still stale at this point),
/// then again afterwards with the actual change flags (Add, Remove,
/// Mode, DesktopShapeChanged, etc.). Skip the begin phase; on the
/// real notification, kick the producer task to refresh bounds.
extern "C" fn display_reconfiguration_callback(_display: u32, flags: u32, user_info: *mut c_void) {
    const K_CG_DISPLAY_BEGIN_CONFIGURATION_FLAG: u32 = 1 << 0;
    if flags & K_CG_DISPLAY_BEGIN_CONFIGURATION_FLAG != 0 {
        return;
    }
    if user_info.is_null() {
        return;
    }
    // SAFETY: user_info is a Box::into_raw of Sender<ProducerEvent>
    // owned by `event_tap_thread`. It's valid for the lifetime of
    // that thread; the registration is removed before the box is
    // freed. The callback only fires while the run loop is running
    // on that thread, so we know the box is live here.
    let sender = unsafe { &*(user_info as *const Sender<ProducerEvent>) };
    if let Err(e) = sender.blocking_send(ProducerEvent::DisplayReconfigured) {
        log::warn!("failed to notify display reconfiguration: {e}");
    }
}

/// Refcon for the screen-unlock distributed-notification observer.
/// Holds the tap's mach-port slot so the callback can re-enable a
/// tap that the lock screen's secure-input mode disabled. Built on
/// the event-tap thread, used only by the callback on the same
/// thread's run loop — never crosses threads after construction.
struct UnlockObserverCtx {
    tap_mach_port: Arc<OnceLock<usize>>,
}

/// CFNotificationCenter callback for `com.apple.screenIsUnlocked`.
/// When the host screen locks, its password field enables secure
/// event input, which disables our session event tap. macOS does
/// not re-enable the tap on unlock — we must, or the daemon keeps
/// running but captures nothing. Re-enabling an already-enabled tap
/// is a documented no-op, so calling this is safe even on the rare
/// unlock where the tap rode through the lock intact.
extern "C" fn screen_unlocked_callback(
    _center: *mut c_void,
    observer: *mut c_void,
    _name: CFStringRef,
    _object: *const c_void,
    _user_info: CFDictionaryRef,
) {
    if observer.is_null() {
        return;
    }
    // SAFETY: `observer` is the `Box::into_raw(Box::new(UnlockObserverCtx))`
    // owned by `event_tap_thread`; valid until the run loop exits and
    // the observer is removed. The callback only fires while the run
    // loop runs on that thread, so the box is live here.
    let ctx = unsafe { &*(observer as *const UnlockObserverCtx) };
    match ctx.tap_mach_port.get() {
        Some(&port) => {
            log::info!("screen unlocked — re-enabling CGEventTap");
            unsafe { CGEventTapEnable(port as *mut c_void, true) };
        }
        None => log::warn!("screen unlocked but tap mach port not yet stored — cannot re-enable"),
    }
}

pub struct MacOSInputCapture {
    event_rx: Receiver<(Position, CaptureEvent)>,
    notify_tx: Sender<ProducerEvent>,
    run_loop: CFRunLoop,
}

impl MacOSInputCapture {
    pub async fn new() -> Result<Self, MacosCaptureCreationError> {
        request_macos_capture_permissions()?;

        let state = Arc::new(Mutex::new(InputCaptureState::new()?));
        // Generously sized: the event-tap callback feeds this from
        // the kernel's input thread and only drops/blocks once it's
        // full (see the try_send path in `create_event_tap`). A deep
        // buffer rides out brief consumer stalls — a slow DTLS send,
        // a GC-like tokio scheduling gap — without dropping motion
        // samples or blocking the callback.
        let (event_tx, event_rx) = mpsc::channel(1024);
        let (notify_tx, mut notify_rx) = mpsc::channel(32);
        let (ready_tx, ready_rx) = std::sync::mpsc::channel();
        let (tap_exit_tx, mut tap_exit_rx) = oneshot::channel();

        unsafe {
            configure_cf_settings()?;
        }

        log::info!("Enabling CGEvent tap");
        let event_tap_thread_state = state.clone();
        let event_tap_notify = notify_tx.clone();
        thread::spawn(move || {
            event_tap_thread(
                event_tap_thread_state,
                event_tx,
                event_tap_notify,
                ready_tx,
                tap_exit_tx,
            )
        });

        // wait for event tap creation result
        let run_loop = ready_rx.recv().expect("channel closed")?;

        let _tap_task: tokio::task::JoinHandle<()> = tokio::task::spawn_local(async move {
            loop {
                tokio::select! {
                    producer_event = notify_rx.recv() => {
                        let Some(producer_event) = producer_event else {
                            break;
                        };
                        let mut state = state.lock().await;
                        state.handle_producer_event(producer_event).await.unwrap_or_else(|e| {
                            log::error!("Failed to handle producer event: {e}");
                        })
                    }
                    _ = &mut tap_exit_rx => break,
                }
            }
            // show cursor
            let _ = CGDisplay::show_cursor(&CGDisplay::main());
        });

        Ok(Self {
            event_rx,
            notify_tx,
            run_loop,
        })
    }
}

fn request_macos_capture_permissions() -> Result<(), MacosCaptureCreationError> {
    // Call both request functions unconditionally so macOS surfaces both
    // TCC prompts on the very first launch. TCC always returns `false` the
    // first time a permission is requested (the grant only becomes visible
    // on the next process launch), so returning early on the first failure
    // would skip the second prompt and force the user through an extra
    // relaunch just to see it.
    let accessibility = request_accessibility_permission();
    let input_monitoring = request_input_monitoring_permission();

    if !accessibility {
        return Err(MacosCaptureCreationError::AccessibilityPermission);
    }
    if !input_monitoring {
        return Err(MacosCaptureCreationError::InputMonitoringPermission);
    }
    Ok(())
}

fn request_accessibility_permission() -> bool {
    // Silent check. The GUI owns the one-time user-visible prompt at
    // startup (see mousehop_gtk::macos_privacy) so retries triggered by
    // clicking the "Reenable" button don't pop a fresh Accessibility
    // alert every time.
    unsafe { AXIsProcessTrusted() }
}

fn request_input_monitoring_permission() -> bool {
    // Silent check, same reasoning as above.
    unsafe { CGPreflightListenEventAccess() }
}

impl Drop for MacOSInputCapture {
    fn drop(&mut self) {
        self.run_loop.stop();
    }
}

#[async_trait]
impl Capture for MacOSInputCapture {
    async fn create(&mut self, pos: Position) -> Result<(), CaptureError> {
        let notify_tx = self.notify_tx.clone();
        tokio::task::spawn_local(async move {
            log::debug!("creating capture, {pos}");
            let _ = notify_tx.send(ProducerEvent::Create(pos)).await;
            log::debug!("done !");
        });
        Ok(())
    }

    async fn destroy(&mut self, pos: Position) -> Result<(), CaptureError> {
        let notify_tx = self.notify_tx.clone();
        tokio::task::spawn_local(async move {
            log::debug!("destroying capture {pos}");
            let _ = notify_tx.send(ProducerEvent::Destroy(pos)).await;
            log::debug!("done !");
        });
        Ok(())
    }

    async fn release(&mut self, warp_target: Option<(i32, i32)>) -> Result<(), CaptureError> {
        log::info!("[release-warp] macOS backend release(warp_target={warp_target:?})");
        let notify_tx = self.notify_tx.clone();
        tokio::task::spawn_local(async move {
            log::debug!("notifying Release");
            let _ = notify_tx.send(ProducerEvent::Release { warp_target }).await;
        });
        Ok(())
    }

    async fn terminate(&mut self) -> Result<(), CaptureError> {
        Ok(())
    }

    fn display_bounds(&self) -> Option<(u32, u32)> {
        // Mirror the InputEmulation-side implementation: the union of
        // every active display's rectangle, in points (which match
        // the units used by CGEvent.location() so the
        // MotionAbsolute math stays internally consistent).
        let displays = CGDisplay::active_displays().ok()?;
        let mut xmin = f64::INFINITY;
        let mut xmax = f64::NEG_INFINITY;
        let mut ymin = f64::INFINITY;
        let mut ymax = f64::NEG_INFINITY;
        for id in displays {
            let bounds = CGDisplay::new(id).bounds();
            xmin = xmin.min(bounds.origin.x);
            xmax = xmax.max(bounds.origin.x + bounds.size.width);
            ymin = ymin.min(bounds.origin.y);
            ymax = ymax.max(bounds.origin.y + bounds.size.height);
        }
        if xmax <= xmin || ymax <= ymin {
            return None;
        }
        Some(((xmax - xmin) as u32, (ymax - ymin) as u32))
    }

    fn display_origin(&self) -> (i32, i32) {
        // Top-left of the union of all active displays. Matters when
        // a secondary monitor is positioned LEFT of (or ABOVE) the
        // primary — the global pointer-coordinate system is anchored
        // at the primary's top-left, so a left-attached external
        // gives cursor x ∈ [-w, 0). Without this offset,
        // host_normalized_cursor / peer_warp_target's clamp(0, 1)
        // silently maps every point on the external to "left edge"
        // and the receiver warps to the wrong column.
        let Ok(displays) = CGDisplay::active_displays() else {
            return (0, 0);
        };
        let mut xmin = f64::INFINITY;
        let mut ymin = f64::INFINITY;
        for id in displays {
            let bounds = CGDisplay::new(id).bounds();
            xmin = xmin.min(bounds.origin.x);
            ymin = ymin.min(bounds.origin.y);
        }
        if xmin.is_infinite() || ymin.is_infinite() {
            return (0, 0);
        }
        (xmin as i32, ymin as i32)
    }
}

impl Stream for MacOSInputCapture {
    type Item = Result<(Position, CaptureEvent), CaptureError>;

    fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
        match ready!(self.event_rx.poll_recv(cx)) {
            None => Poll::Ready(None),
            Some(e) => Poll::Ready(Some(Ok(e))),
        }
    }
}

type CGSConnectionID = u32;

#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
    fn CGSSetConnectionProperty(
        cid: CGSConnectionID,
        targetCID: CGSConnectionID,
        key: CFStringRef,
        value: CFBooleanRef,
    ) -> CGError;
    fn _CGSDefaultConnection() -> CGSConnectionID;
}

type CFDictionaryRef = *mut c_void;

#[link(name = "CoreGraphics", kind = "framework")]
extern "C" {
    fn CGSessionCopyCurrentDictionary() -> CFDictionaryRef;
}

#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
    fn CFDictionaryGetValue(dict: CFDictionaryRef, key: *const c_void) -> *const c_void;
    fn CFBooleanGetValue(boolean: CFBooleanRef) -> bool;

    /// The system-wide distributed notification center. Carries
    /// cross-process notifications such as `com.apple.screenIsLocked`
    /// and `com.apple.screenIsUnlocked`.
    fn CFNotificationCenterGetDistributedCenter() -> *mut c_void;
    /// Register `callback` for `name`, delivered on the run loop of
    /// the registering thread. `observer` is an opaque key reused by
    /// the removal call and handed back to the callback.
    fn CFNotificationCenterAddObserver(
        center: *mut c_void,
        observer: *const c_void,
        callback: extern "C" fn(
            *mut c_void,
            *mut c_void,
            CFStringRef,
            *const c_void,
            CFDictionaryRef,
        ),
        name: CFStringRef,
        object: *const c_void,
        suspension_behavior: isize,
    );
    fn CFNotificationCenterRemoveEveryObserver(center: *mut c_void, observer: *const c_void);
}

extern "C" {
    fn CGEventSourceSetLocalEventsSuppressionInterval(
        event_source: CGEventSource,
        seconds: CFTimeInterval,
    );
    fn CGPreflightListenEventAccess() -> bool;
    /// Re-enable an event tap that was disabled by a
    /// `kCGEventTapDisabledByTimeout` event. The Apple-documented
    /// recovery path: see Quartz Event Services Reference. The `tap`
    /// argument is a `CFMachPortRef`; we pass the raw pointer so we
    /// can store it as `usize` for cross-thread sharing.
    fn CGEventTapEnable(tap: *mut c_void, enable: bool);

    /// Register a callback invoked when the display configuration
    /// changes (monitor add/remove, resolution change, mirror,
    /// rearrange, etc). See Quartz Display Services Reference.
    fn CGDisplayRegisterReconfigurationCallback(
        callback: extern "C" fn(u32, u32, *mut c_void),
        user_info: *mut c_void,
    ) -> CGError;
    fn CGDisplayRemoveReconfigurationCallback(
        callback: extern "C" fn(u32, u32, *mut c_void),
        user_info: *mut c_void,
    ) -> CGError;
}

#[link(name = "IOKit", kind = "framework")]
extern "C" {
    /// Register the calling process for system-power notifications.
    /// Returns the `io_connect_t` root power port (used later in
    /// `IOAllowPowerChange` to ack sleep-related messages) and writes
    /// the notification port + an `io_object_t` notifier through the
    /// out-pointers. The returned notification port carries a
    /// CFRunLoopSource we attach to this thread's run loop so the
    /// callback fires inline with the existing event-tap loop.
    fn IORegisterForSystemPower(
        refcon: *mut c_void,
        port_ref: *mut *mut c_void,
        callback: extern "C" fn(*mut c_void, u32, u32, *mut c_void),
        notifier: *mut u32,
    ) -> u32;
    fn IODeregisterForSystemPower(notifier: *mut u32) -> i32;
    fn IONotificationPortGetRunLoopSource(notify: *mut c_void) -> CFRunLoopSourceRef;
    fn IONotificationPortDestroy(notify: *mut c_void);
    /// Ack a kIOMessageCanSystemSleep / kIOMessageSystemWillSleep so
    /// the OS doesn't stall on its 30s default timeout waiting for us.
    /// Required even when we have no objection — silence is treated as
    /// "still thinking" by the kernel.
    fn IOAllowPowerChange(kernel_port: u32, notification_id: isize) -> i32;
}

#[link(name = "ApplicationServices", kind = "framework")]
extern "C" {
    fn AXIsProcessTrusted() -> bool;
}

/// Read `com.apple.swipescrolldirection` from the global preferences
/// domain. Returns `true` when Natural Scrolling is enabled (the
/// modern macOS default) — the same default macOS uses if the key
/// is unset. Used to decide whether to invert scroll deltas before
/// forwarding them to a peer that has its own fixed convention.
fn natural_scrolling_enabled() -> bool {
    unsafe {
        let key_cstr = CString::new("com.apple.swipescrolldirection").unwrap();
        let key = CFStringCreateWithCString(
            kCFAllocatorDefault,
            key_cstr.as_ptr() as *const c_char,
            kCFStringEncodingUTF8,
        );
        if key.is_null() {
            return true;
        }
        let value = CFPreferencesCopyAppValue(key, kCFPreferencesAnyApplication);
        CFRelease(key as *const c_void);
        if value.is_null() {
            // Key absent → modern macOS default is enabled.
            return true;
        }
        // The preference is stored as a CFBoolean; kCFBooleanTrue
        // and kCFBooleanFalse are singleton instances, so a pointer
        // compare is correct and sufficient.
        let is_true = (value as CFBooleanRef) == kCFBooleanTrue;
        CFRelease(value);
        is_true
    }
}

#[link(name = "CoreFoundation", kind = "framework")]
extern "C" {
    fn CFPreferencesCopyAppValue(key: CFStringRef, application_id: CFStringRef) -> *const c_void;
    static kCFPreferencesAnyApplication: CFStringRef;
}

unsafe fn configure_cf_settings() -> Result<(), MacosCaptureCreationError> {
    // When we warp the cursor using CGWarpMouseCursorPosition local events are suppressed for a short time
    // this leeds to the cursor not flowing when crossing back from a clinet, set this to to 0 stops the warp
    // from working, set a low value by trial and error, 0.05s seems good. 0.25s is the default
    let event_source = CGEventSource::new(CGEventSourceStateID::CombinedSessionState)
        .map_err(|_| MacosCaptureCreationError::EventSourceCreation)?;
    CGEventSourceSetLocalEventsSuppressionInterval(event_source, 0.05);
    // FIXME Memory Leak

    // This is a private settings that allows the cursor to be hidden while in the background.
    // It is used by Barrier and other apps.
    let key = CString::new("SetsCursorInBackground").unwrap();
    let cf_key = CFStringCreateWithCString(
        kCFAllocatorDefault,
        key.as_ptr() as *const c_char,
        kCFStringEncodingUTF8,
    );
    if CGSSetConnectionProperty(
        _CGSDefaultConnection(),
        _CGSDefaultConnection(),
        cf_key,
        kCFBooleanTrue,
    ) != kCGErrorSuccess
    {
        return Err(MacosCaptureCreationError::CGCursorProperty);
    }
    CFRelease(cf_key as *const c_void);
    Ok(())
}

// From X11/X.h
bitflags! {
    #[repr(C)]
    #[derive(Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd)]
    struct XMods: u32 {
        const ShiftMask = (1<<0);
        const LockMask = (1<<1);
        const ControlMask = (1<<2);
        const Mod1Mask = (1<<3);
        const Mod2Mask = (1<<4);
        const Mod3Mask = (1<<5);
        const Mod4Mask = (1<<6);
        const Mod5Mask = (1<<7);
    }
}
