//! Wayland backend: layer-shell overlay with pointer tracking.
//!
//! The input region is dynamically updated to be the full screen MINUS a
//! hole around the cursor. This lets the cursor pass through to the desktop
//! while still letting us track its position: when the cursor moves outside
//! the hole, it lands on our surface, we get the event, punch a new hole,
//! and the cursor passes through again.

use std::collections::VecDeque;
use std::ptr::NonNull;
use std::time::Duration;

use wayland_client::{
    delegate_noop,
    protocol::{
        wl_buffer, wl_compositor, wl_keyboard, wl_pointer, wl_region, wl_registry, wl_seat,
        wl_shm, wl_shm_pool, wl_surface,
    },
    Connection, Dispatch, EventQueue, Proxy, QueueHandle, WEnum,
};
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};

use anyhow::{Context, Result};
use raw_window_handle::{
    RawDisplayHandle, RawWindowHandle, WaylandDisplayHandle, WaylandWindowHandle,
};

/// Size of the passthrough hole around the cursor. Should be large enough
/// that normal mouse movement stays within the hole between frames.
const CURSOR_HOLE_SIZE: i32 = 512;

pub enum OverlayEvent {
    Close,
    CursorMoved(f64, f64),
    #[allow(dead_code)]
    Configure(u32, u32),
}

pub struct WaylandBackend {
    state: WaylandState,
    event_queue: EventQueue<WaylandState>,
    queue_handle: QueueHandle<WaylandState>,
}

impl WaylandBackend {
    pub fn new() -> Result<Self> {
        let conn = Connection::connect_to_env().context("connect to Wayland display")?;
        let mut event_queue = conn.new_event_queue();
        let queue_handle = event_queue.handle();

        conn.display().get_registry(&queue_handle, ());

        let mut state = WaylandState {
            conn,
            compositor: None,
            shm: None,
            surface: None,
            layer_shell: None,
            layer_surface: None,
            seat: None,
            screen_w: 0,
            screen_h: 0,
            configured: false,
            pending_events: VecDeque::new(),
        };

        event_queue
            .roundtrip(&mut state)
            .context("initial roundtrip")?;

        if state.compositor.is_none() {
            anyhow::bail!("compositor does not advertise wl_compositor");
        }
        if state.layer_shell.is_none() {
            anyhow::bail!(
                "compositor does not support wlr-layer-shell-unstable-v1. \
                 Supported compositors: Sway, Hyprland, river, etc."
            );
        }

        state.init_layer_surface(&queue_handle)?;
        event_queue
            .roundtrip(&mut state)
            .context("roundtrip after layer surface")?;

        while !state.configured {
            event_queue
                .blocking_dispatch(&mut state)
                .context("waiting for configure")?;
        }

        Ok(Self {
            state,
            event_queue,
            queue_handle,
        })
    }

    pub fn screen_size(&self) -> (u32, u32) {
        (self.state.screen_w, self.state.screen_h)
    }

    pub fn raw_display_handle(&self) -> Result<RawDisplayHandle> {
        let ptr = self.state.conn.backend().display_ptr() as *mut std::ffi::c_void;
        let nn = NonNull::new(ptr).context("wl_display pointer is null")?;
        Ok(RawDisplayHandle::Wayland(WaylandDisplayHandle::new(nn)))
    }

    pub fn raw_window_handle(&self) -> Result<RawWindowHandle> {
        let surface = self.state.surface.as_ref().context("no wl_surface")?;
        let ptr = surface.id().as_ptr() as *mut std::ffi::c_void;
        let nn = NonNull::new(ptr).context("wl_surface pointer is null")?;
        Ok(RawWindowHandle::Wayland(WaylandWindowHandle::new(nn)))
    }

    /// Update the input region to punch a passthrough hole at the cursor.
    pub fn update_cursor_hole(&mut self, cx: f64, cy: f64) -> Result<()> {
        let compositor = self
            .state
            .compositor
            .as_ref()
            .context("wl_compositor not available")?;
        let surface = self.state.surface.as_ref().context("no wl_surface")?;

        let w = self.state.screen_w as i32;
        let h = self.state.screen_h as i32;
        let half = CURSOR_HOLE_SIZE / 2;
        let hx = (cx as i32).clamp(0, w);
        let hy = (cy as i32).clamp(0, h);

        // Full screen minus a hole around the cursor.
        let region = compositor.create_region(&self.queue_handle, ());
        region.add(0, 0, w, h);
        region.subtract(hx - half, hy - half, CURSOR_HOLE_SIZE, CURSOR_HOLE_SIZE);

        surface.set_input_region(Some(&region));
        surface.commit();
        self.state.conn.flush().context("flush after cursor hole")?;
        Ok(())
    }

    pub fn next_event(&mut self, timeout: Duration) -> Result<Option<OverlayEvent>> {
        if let Some(ev) = self.state.pending_events.pop_front() {
            return Ok(Some(ev));
        }
        if self.state.surface.is_none() {
            return Ok(None);
        }

        self.event_queue
            .flush()
            .context("flush before poll")?;

        self.event_queue
            .dispatch_pending(&mut self.state)
            .context("dispatch_pending (pre-poll)")?;
        if let Some(ev) = self.state.pending_events.pop_front() {
            self.state.pending_events.push_front(ev);
        } else {
            match self.event_queue.prepare_read() {
                Some(read_guard) => {
                    let fd = read_guard.connection_fd();
                    let mut pollfd =
                        [nix::poll::PollFd::new(fd, nix::poll::PollFlags::POLLIN)];
                    let millis = timeout.as_millis().min(u16::MAX as u128) as u16;
                    let n = match nix::poll::poll(&mut pollfd, millis) {
                        Ok(n) => n,
                        Err(nix::errno::Errno::EINTR) => {
                            // Signal interrupted poll — return so the loop
                            // can check should_exit().
                            drop(read_guard);
                            return Ok(None);
                        }
                        Err(e) => return Err(e).context("poll"),
                    };

                    if n > 0 {
                        let _ = read_guard.read();
                    } else {
                        drop(read_guard);
                        return Ok(None);
                    }

                    self.event_queue
                        .dispatch_pending(&mut self.state)
                        .context("dispatch_pending (post-read)")?;
                }
                None => {
                    self.event_queue
                        .dispatch_pending(&mut self.state)
                        .context("dispatch_pending (no-read)")?;
                }
            }
        }

        // Coalesce CursorMoved — only keep the latest position.
        let mut last_cursor: Option<(f64, f64)> = None;
        let mut first_non_cursor: Option<OverlayEvent> = None;
        while let Some(ev) = self.state.pending_events.pop_front() {
            match ev {
                OverlayEvent::CursorMoved(x, y) => last_cursor = Some((x, y)),
                other => {
                    first_non_cursor = Some(other);
                    break;
                }
            }
        }
        if let Some(ev) = first_non_cursor {
            if let Some((x, y)) = last_cursor {
                self.state
                    .pending_events
                    .push_front(OverlayEvent::CursorMoved(x, y));
            }
            return Ok(Some(ev));
        }
        if let Some((x, y)) = last_cursor {
            return Ok(Some(OverlayEvent::CursorMoved(x, y)));
        }
        Ok(None)
    }
}

impl Drop for WaylandBackend {
    fn drop(&mut self) {
        if let Some(ls) = self.state.layer_surface.take() {
            ls.destroy();
        }
        if let Some(s) = self.state.surface.take() {
            s.destroy();
        }
        let _ = self.state.conn.flush();
    }
}

// ===========================================================================
// Internal state.
// ===========================================================================

struct WaylandState {
    conn: Connection,

    compositor: Option<wl_compositor::WlCompositor>,
    #[allow(dead_code)]
    shm: Option<wl_shm::WlShm>,
    seat: Option<wl_seat::WlSeat>,
    layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,

    surface: Option<wl_surface::WlSurface>,
    layer_surface: Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>,

    screen_w: u32,
    screen_h: u32,
    configured: bool,

    pending_events: VecDeque<OverlayEvent>,
}

impl WaylandState {
    fn init_layer_surface(&mut self, qh: &QueueHandle<Self>) -> Result<()> {
        let compositor = self
            .compositor
            .as_ref()
            .context("wl_compositor not available")?;
        let layer_shell = self
            .layer_shell
            .as_ref()
            .context("zwlr_layer_shell_v1 not available")?;

        let surface = compositor.create_surface(qh, ());

        // Start with full-screen input region. The cursor hole will be
        // punched on the first pointer event.
        let input_region = compositor.create_region(qh, ());
        let w = i32::MAX;
        let h = i32::MAX;
        input_region.add(0, 0, w, h);
        surface.set_input_region(Some(&input_region));

        let layer_surface = layer_shell.get_layer_surface(
            &surface,
            None,
            zwlr_layer_shell_v1::Layer::Overlay,
            "ringa".to_string(),
            qh,
            (),
        );

        layer_surface.set_size(0, 0);
        layer_surface.set_anchor(
            zwlr_layer_surface_v1::Anchor::Top
                | zwlr_layer_surface_v1::Anchor::Bottom
                | zwlr_layer_surface_v1::Anchor::Left
                | zwlr_layer_surface_v1::Anchor::Right,
        );
        layer_surface.set_exclusive_zone(-1);
        layer_surface
            .set_keyboard_interactivity(zwlr_layer_surface_v1::KeyboardInteractivity::None);

        surface.commit();

        self.surface = Some(surface);
        self.layer_surface = Some(layer_surface);
        Ok(())
    }
}

// ===========================================================================
// Wayland dispatch implementations.
// ===========================================================================

impl Dispatch<wl_registry::WlRegistry, ()> for WaylandState {
    fn event(
        state: &mut Self,
        registry: &wl_registry::WlRegistry,
        event: wl_registry::Event,
        _: &(),
        _: &Connection,
        qh: &QueueHandle<Self>,
    ) {
        let wl_registry::Event::Global {
            name,
            interface,
            version,
        } = event
        else {
            return;
        };
        match interface.as_str() {
            "wl_compositor" => {
                state.compositor = Some(registry.bind(name, version.min(4), qh, ()));
            }
            "wl_shm" => {
                state.shm = Some(registry.bind(name, 1, qh, ()));
            }
            "wl_seat" => {
                state.seat = Some(registry.bind(name, version.min(7), qh, ()));
            }
            "zwlr_layer_shell_v1" => {
                state.layer_shell = Some(registry.bind(name, version.min(4), qh, ()));
            }
            _ => {}
        }
    }
}

impl Dispatch<wl_seat::WlSeat, ()> for WaylandState {
    fn event(
        _state: &mut Self,
        seat: &wl_seat::WlSeat,
        event: wl_seat::Event,
        _: &(),
        _: &Connection,
        qh: &QueueHandle<Self>,
    ) {
        if let wl_seat::Event::Capabilities {
            capabilities: WEnum::Value(caps),
        } = event
        {
            if caps.contains(wl_seat::Capability::Pointer) {
                seat.get_pointer(qh, ());
            }
        }
    }
}

impl Dispatch<wl_pointer::WlPointer, ()> for WaylandState {
    fn event(
        state: &mut Self,
        _: &wl_pointer::WlPointer,
        event: wl_pointer::Event,
        _: &(),
        _: &Connection,
        _: &QueueHandle<Self>,
    ) {
        match event {
            wl_pointer::Event::Enter {
                surface_x,
                surface_y,
                ..
            }
            | wl_pointer::Event::Motion {
                surface_x,
                surface_y,
                ..
            } => {
                state
                    .pending_events
                    .push_back(OverlayEvent::CursorMoved(surface_x, surface_y));
            }
            _ => {}
        }
    }
}

impl Dispatch<wl_keyboard::WlKeyboard, ()> for WaylandState {
    fn event(
        _: &mut Self,
        _: &wl_keyboard::WlKeyboard,
        _: wl_keyboard::Event,
        _: &(),
        _: &Connection,
        _: &QueueHandle<Self>,
    ) {
    }
}

impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ()> for WaylandState {
    fn event(
        state: &mut Self,
        layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
        event: zwlr_layer_surface_v1::Event,
        _: &(),
        _: &Connection,
        _: &QueueHandle<Self>,
    ) {
        match event {
            zwlr_layer_surface_v1::Event::Configure {
                serial,
                width,
                height,
            } => {
                layer_surface.ack_configure(serial);
                const MAX_SCREEN_DIM: u32 = 16384;
                if width > 0 && height > 0 && width <= MAX_SCREEN_DIM && height <= MAX_SCREEN_DIM {
                    state.screen_w = width;
                    state.screen_h = height;
                    state.configured = true;
                    state
                        .pending_events
                        .push_back(OverlayEvent::Configure(width, height));
                }
            }
            zwlr_layer_surface_v1::Event::Closed => {
                state.pending_events.push_back(OverlayEvent::Close);
            }
            _ => {}
        }
    }
}

delegate_noop!(WaylandState: ignore wl_compositor::WlCompositor);
delegate_noop!(WaylandState: ignore wl_region::WlRegion);
delegate_noop!(WaylandState: ignore wl_surface::WlSurface);
delegate_noop!(WaylandState: ignore wl_shm::WlShm);
delegate_noop!(WaylandState: ignore wl_shm_pool::WlShmPool);
delegate_noop!(WaylandState: ignore wl_buffer::WlBuffer);
delegate_noop!(WaylandState: ignore zwlr_layer_shell_v1::ZwlrLayerShellV1);
