// Copyright (C) 2026 alto555
// SPDX-License-Identifier: GPL-2.0-only

//! A minimal X11 client, spoken directly over the socket.
//!
//! No windowing library is available on this machine (no package downloads),
//! so the window layer is the X11 wire protocol by hand: connection setup
//! with the MIT-MAGIC-COOKIE from `.Xauthority`, one window, one graphics
//! context, `PutImage` to blit a frame, and an event loop.
//!
//! That is less mad than it sounds for this application.  The renderer
//! already produces a finished RGB frame; all a window really has to do is
//! show that frame and report mouse and key events.  Everything below is
//! about 500 lines and depends on nothing.

use std::io::{Read, Write};
use std::os::unix::net::UnixStream;

pub struct Window {
    sock: UnixStream,
    wid: u32,
    gc: u32,
    /// Additional top-level windows on the same connection: (id, gc).
    extra: Vec<(u32, u32)>,
    root: u32,
    wm_protocols: u32,
    #[allow(dead_code)]
    root_visual: u32,
    depth: u8,
    max_req_bytes: usize,
    pub width: u16,
    pub height: u16,
    next_id: u32,
    id_base: u32,
    id_mask: u32,
    wm_delete: u32,
    keymap: Vec<Vec<u32>>,
    min_keycode: u8,
    pending: Vec<Event>,
    /// Previous frame of the main window, for damage tracking.
    last_frame: Vec<u8>,
    last_dims: (usize, usize),
    /// Bytes pushed by the most recent present — the number that matters on
    /// a remote display, where the wire is the bottleneck, not the drawing.
    pub last_present_bytes: usize,
    screen: (u16, u16),
}

/// An event plus the window it landed on.
///
/// Once there is more than one window, "which window" is not optional — a
/// close on the help window must not quit the application.
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Event {
    pub window: u32,
    pub kind: Kind,
}

#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Kind {
    Expose,
    Resize(u16, u16),
    /// Keysym, plus whether shift/ctrl were held.
    Key(u32, bool, bool),
    /// button, x, y
    /// button, x, y, modifier state (bit 0 = shift, bit 2 = control)
    ButtonPress(u8, i16, i16, u16),
    ButtonRelease(u8, i16, i16),
    Motion(i16, i16, u16),
    Close,
}

// Common keysyms, so callers do not deal in raw numbers.
pub const K_ESC: u32 = 0xff1b;
pub const K_LEFT: u32 = 0xff51;
pub const K_UP: u32 = 0xff52;
pub const K_RIGHT: u32 = 0xff53;
pub const K_DOWN: u32 = 0xff54;
pub const K_HOME: u32 = 0xff50;
pub const K_PGUP: u32 = 0xff55;
pub const K_PGDN: u32 = 0xff56;
pub const K_DEL: u32 = 0xffff;
pub const K_BACKSPACE: u32 = 0xff08;

fn pad4(n: usize) -> usize {
    (4 - (n % 4)) % 4
}

/// Find the MIT-MAGIC-COOKIE for this display number in `.Xauthority`.
fn auth_cookie(display: &str) -> Option<Vec<u8>> {
    let path = std::env::var("XAUTHORITY")
        .ok()
        .or_else(|| std::env::var("HOME").ok().map(|h| format!("{}/.Xauthority", h)))?;
    let d = std::fs::read(path).ok()?;
    let mut i = 0usize;
    let rd16 = |d: &[u8], i: usize| -> usize { ((d[i] as usize) << 8) | d[i + 1] as usize };
    while i + 2 <= d.len() {
        i += 2; // family
        let mut fields: Vec<Vec<u8>> = Vec::new();
        for _ in 0..4 {
            if i + 2 > d.len() {
                return None;
            }
            let n = rd16(&d, i);
            i += 2;
            if i + n > d.len() {
                return None;
            }
            fields.push(d[i..i + n].to_vec());
            i += n;
        }
        if fields[1] == display.as_bytes() && fields[2] == b"MIT-MAGIC-COOKIE-1" {
            return Some(fields[3].clone());
        }
    }
    None
}

impl Window {
    pub fn open(title: &str, w: u16, h: u16) -> std::io::Result<Window> {
        let disp = std::env::var("DISPLAY").unwrap_or_else(|_| ":0".into());
        let num = disp.trim_start_matches(':').split('.').next().unwrap_or("0").to_string();
        let mut sock = UnixStream::connect(format!("/tmp/.X11-unix/X{}", num))?;

        let cookie = auth_cookie(&num).unwrap_or_default();
        let name: &[u8] = if cookie.is_empty() { b"" } else { b"MIT-MAGIC-COOKIE-1" };

        // --- connection setup -------------------------------------------
        let mut req = Vec::new();
        req.push(b'l');
        req.push(0);
        req.extend_from_slice(&11u16.to_le_bytes());
        req.extend_from_slice(&0u16.to_le_bytes());
        req.extend_from_slice(&(name.len() as u16).to_le_bytes());
        req.extend_from_slice(&(cookie.len() as u16).to_le_bytes());
        req.extend_from_slice(&0u16.to_le_bytes());
        req.extend_from_slice(name);
        req.resize(req.len() + pad4(name.len()), 0);
        req.extend_from_slice(&cookie);
        req.resize(req.len() + pad4(cookie.len()), 0);
        sock.write_all(&req)?;

        let mut head = [0u8; 8];
        sock.read_exact(&mut head)?;
        let extra = u16::from_le_bytes([head[6], head[7]]) as usize * 4;
        let mut body = vec![0u8; extra];
        sock.read_exact(&mut body)?;
        if head[0] != 1 {
            let n = head[1] as usize;
            let msg = String::from_utf8_lossy(&body[..n.min(body.len())]).into_owned();
            return Err(std::io::Error::new(
                std::io::ErrorKind::PermissionDenied,
                format!("X11 setup refused: {}", msg),
            ));
        }

        let rd32 = |b: &[u8], o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
        let rd16 = |b: &[u8], o: usize| u16::from_le_bytes([b[o], b[o + 1]]);
        let id_base = rd32(&body, 4);
        let id_mask = rd32(&body, 8);
        let vendor_len = rd16(&body, 16) as usize;
        let max_req_units = rd16(&body, 18) as usize;
        let num_formats = body[21] as usize;
        let min_keycode = body[26];
        let max_keycode = body[27];

        let mut o = 32 + vendor_len + pad4(vendor_len);
        // pixmap formats: 8 bytes each
        let mut bpp_for_depth = [0u8; 33];
        for k in 0..num_formats {
            let d = body[o + k * 8];
            let bpp = body[o + k * 8 + 1];
            if (d as usize) < 33 {
                bpp_for_depth[d as usize] = bpp;
            }
        }
        o += num_formats * 8;

        // first screen
        let root = rd32(&body, o);
        // SCREEN: root, colormap, white, black, input_masks = 20 bytes, then
        // width_in_pixels, height_in_pixels.
        let screen_w = rd16(&body, o + 20);
        let screen_h = rd16(&body, o + 22);
        let root_visual = rd32(&body, o + 32);
        let root_depth = body[o + 38];

        let mut win = Window {
            sock,
            wid: 0,
            gc: 0,
            extra: Vec::new(),
            root,
            wm_protocols: 0,
            root_visual,
            depth: root_depth,
            max_req_bytes: max_req_units * 4,
            width: w,
            height: h,
            next_id: 0,
            id_base,
            id_mask,
            wm_delete: 0,
            keymap: Vec::new(),
            min_keycode,
            pending: Vec::new(),
            last_frame: Vec::new(),
            last_dims: (0, 0),
            last_present_bytes: 0,
            screen: (screen_w, screen_h),
        };
        if bpp_for_depth[root_depth as usize] != 32 {
            // Everything below assumes 32-bit ZPixmap, which is what every
            // modern server uses for depth 24.  Fail loudly rather than
            // painting garbage.
            return Err(std::io::Error::new(
                std::io::ErrorKind::Unsupported,
                format!(
                    "depth {} uses {} bits per pixel; only 32 is handled",
                    root_depth, bpp_for_depth[root_depth as usize]
                ),
            ));
        }

        win.wid = win.new_id();
        win.gc = win.new_id();

        // CreateWindow
        let mut r = Vec::new();
        r.push(1u8);
        r.push(root_depth);
        r.extend_from_slice(&10u16.to_le_bytes()); // request length in units
        r.extend_from_slice(&win.wid.to_le_bytes());
        r.extend_from_slice(&root.to_le_bytes());
        r.extend_from_slice(&0i16.to_le_bytes());
        r.extend_from_slice(&0i16.to_le_bytes());
        r.extend_from_slice(&w.to_le_bytes());
        r.extend_from_slice(&h.to_le_bytes());
        r.extend_from_slice(&0u16.to_le_bytes()); // border
        r.extend_from_slice(&1u16.to_le_bytes()); // InputOutput
        r.extend_from_slice(&root_visual.to_le_bytes());
        r.extend_from_slice(&0x0000_0802u32.to_le_bytes()); // BackPixel | EventMask
        r.extend_from_slice(&0x0015_1517u32.to_le_bytes()); // background
        // Exposure|KeyPress|ButtonPress|ButtonRelease|PointerMotion|StructureNotify
        let mask: u32 = 0x8000 | 0x1 | 0x4 | 0x8 | 0x40 | 0x2_0000;
        r.extend_from_slice(&mask.to_le_bytes());
        win.send(&r)?;

        // CreateGC
        let mut r = Vec::new();
        r.push(55u8);
        r.push(0);
        r.extend_from_slice(&4u16.to_le_bytes());
        r.extend_from_slice(&win.gc.to_le_bytes());
        r.extend_from_slice(&win.wid.to_le_bytes());
        r.extend_from_slice(&0u32.to_le_bytes());
        win.send(&r)?;

        win.set_title(title)?;

        // WM_PROTOCOLS / WM_DELETE_WINDOW so the close button works.
        let proto = win.intern_atom("WM_PROTOCOLS")?;
        let del = win.intern_atom("WM_DELETE_WINDOW")?;
        win.wm_delete = del;
        win.wm_protocols = proto;
        let mut r = Vec::new();
        r.push(18u8);
        r.push(0);
        r.extend_from_slice(&7u16.to_le_bytes());
        r.extend_from_slice(&win.wid.to_le_bytes());
        r.extend_from_slice(&proto.to_le_bytes());
        r.extend_from_slice(&4u32.to_le_bytes()); // ATOM
        r.push(32);
        r.extend_from_slice(&[0, 0, 0]);
        r.extend_from_slice(&1u32.to_le_bytes());
        r.extend_from_slice(&del.to_le_bytes());
        win.send(&r)?;

        win.load_keymap(min_keycode, max_keycode)?;

        // MapWindow
        let mut r = Vec::new();
        r.push(8u8);
        r.push(0);
        r.extend_from_slice(&2u16.to_le_bytes());
        r.extend_from_slice(&win.wid.to_le_bytes());
        win.send(&r)?;
        Ok(win)
    }

    /// Root window size in pixels — used to keep generated windows on screen.
    pub fn screen_size(&self) -> (u16, u16) {
        self.screen
    }

    pub fn id(&self) -> u32 {
        self.wid
    }

    /// Round-trip to the server so everything sent so far is processed.
    ///
    /// Without this the client writes its requests and moves on.  A local
    /// server picks them up straight away, but a remote or VNC one can hold
    /// batched updates until some other event prods it — so a repaint driven
    /// purely by a keystroke could sit invisible until the next mouse click.
    /// One round trip per frame is cheap next to a repaint that never arrives.
    pub fn sync(&mut self) -> std::io::Result<()> {
        // GetInputFocus: smallest request that demands a reply.
        let r = [43u8, 0, 1, 0];
        self.send(&r)?;
        self.sock.flush()?;
        let _ = self.wait_reply()?;
        Ok(())
    }

    /// Forget the cached frame so the next present pushes everything.
    ///
    /// Required on Expose: X keeps no copy of an obscured window, so pixels
    /// that another window covered are gone even though the frame buffer
    /// still matches — damage tracking would correctly conclude "nothing
    /// changed" and leave the hole on screen.
    pub fn invalidate(&mut self) {
        self.last_frame.clear();
    }

    /// Open another top-level window on the same connection.
    ///
    /// One connection rather than one per window, so a single blocking read
    /// still serves every window — two sockets would need polling.
    pub fn open_child(&mut self, title: &str, w: u16, h: u16) -> std::io::Result<u32> {
        let wid = self.new_id();
        let gc = self.new_id();
        let root = self.root;
        let depth = self.depth;
        let visual = self.root_visual;

        let mut r = Vec::new();
        r.push(1u8);
        r.push(depth);
        r.extend_from_slice(&10u16.to_le_bytes());
        r.extend_from_slice(&wid.to_le_bytes());
        r.extend_from_slice(&root.to_le_bytes());
        r.extend_from_slice(&40i16.to_le_bytes());
        r.extend_from_slice(&40i16.to_le_bytes());
        r.extend_from_slice(&w.to_le_bytes());
        r.extend_from_slice(&h.to_le_bytes());
        r.extend_from_slice(&0u16.to_le_bytes());
        r.extend_from_slice(&1u16.to_le_bytes());
        r.extend_from_slice(&visual.to_le_bytes());
        r.extend_from_slice(&0x0000_0802u32.to_le_bytes());
        r.extend_from_slice(&0x0015_1517u32.to_le_bytes());
        let mask: u32 = 0x8000 | 0x1 | 0x4 | 0x2_0000;
        r.extend_from_slice(&mask.to_le_bytes());
        self.send(&r)?;

        let mut r = Vec::new();
        r.push(55u8);
        r.push(0);
        r.extend_from_slice(&4u16.to_le_bytes());
        r.extend_from_slice(&gc.to_le_bytes());
        r.extend_from_slice(&wid.to_le_bytes());
        r.extend_from_slice(&0u32.to_le_bytes());
        self.send(&r)?;

        let t = title.as_bytes();
        let mut r = Vec::new();
        r.push(18u8);
        r.push(0);
        r.extend_from_slice(&(6 + ((t.len() + pad4(t.len())) / 4) as u16).to_le_bytes());
        r.extend_from_slice(&wid.to_le_bytes());
        r.extend_from_slice(&39u32.to_le_bytes());
        r.extend_from_slice(&31u32.to_le_bytes());
        r.push(8);
        r.extend_from_slice(&[0, 0, 0]);
        r.extend_from_slice(&(t.len() as u32).to_le_bytes());
        r.extend_from_slice(t);
        r.resize(r.len() + pad4(t.len()), 0);
        self.send(&r)?;

        // So the window manager's close button reaches us here too.
        let proto = self.wm_protocols;
        let del = self.wm_delete;
        let mut r = Vec::new();
        r.push(18u8);
        r.push(0);
        r.extend_from_slice(&7u16.to_le_bytes());
        r.extend_from_slice(&wid.to_le_bytes());
        r.extend_from_slice(&proto.to_le_bytes());
        r.extend_from_slice(&4u32.to_le_bytes());
        r.push(32);
        r.extend_from_slice(&[0, 0, 0]);
        r.extend_from_slice(&1u32.to_le_bytes());
        r.extend_from_slice(&del.to_le_bytes());
        self.send(&r)?;

        let mut r = Vec::new();
        r.push(8u8);
        r.push(0);
        r.extend_from_slice(&2u16.to_le_bytes());
        r.extend_from_slice(&wid.to_le_bytes());
        self.send(&r)?;

        self.extra.push((wid, gc));
        Ok(wid)
    }

    pub fn destroy(&mut self, wid: u32) -> std::io::Result<()> {
        self.extra.retain(|(w, _)| *w != wid);
        let mut r = Vec::new();
        r.push(4u8);
        r.push(0);
        r.extend_from_slice(&2u16.to_le_bytes());
        r.extend_from_slice(&wid.to_le_bytes());
        self.send(&r)
    }

    fn new_id(&mut self) -> u32 {
        let id = self.id_base | (self.next_id & self.id_mask);
        self.next_id += 1;
        id
    }

    fn send(&mut self, b: &[u8]) -> std::io::Result<()> {
        self.sock.write_all(b)
    }

    pub fn set_title(&mut self, title: &str) -> std::io::Result<()> {
        let t = title.as_bytes();
        let mut r = Vec::new();
        r.push(18u8);
        r.push(0); // Replace
        r.extend_from_slice(&(6 + ((t.len() + pad4(t.len())) / 4) as u16).to_le_bytes());
        r.extend_from_slice(&self.wid.to_le_bytes());
        r.extend_from_slice(&39u32.to_le_bytes()); // WM_NAME
        r.extend_from_slice(&31u32.to_le_bytes()); // STRING
        r.push(8);
        r.extend_from_slice(&[0, 0, 0]);
        r.extend_from_slice(&(t.len() as u32).to_le_bytes());
        r.extend_from_slice(t);
        r.resize(r.len() + pad4(t.len()), 0);
        self.send(&r)
    }

    fn intern_atom(&mut self, name: &str) -> std::io::Result<u32> {
        let n = name.as_bytes();
        let mut r = Vec::new();
        r.push(16u8);
        r.push(0);
        r.extend_from_slice(&(2 + ((n.len() + pad4(n.len())) / 4) as u16).to_le_bytes());
        r.extend_from_slice(&(n.len() as u16).to_le_bytes());
        r.extend_from_slice(&0u16.to_le_bytes());
        r.extend_from_slice(n);
        r.resize(r.len() + pad4(n.len()), 0);
        self.send(&r)?;
        let rep = self.wait_reply()?;
        Ok(u32::from_le_bytes([rep[8], rep[9], rep[10], rep[11]]))
    }

    fn load_keymap(&mut self, min: u8, max: u8) -> std::io::Result<()> {
        let count = (max - min) as u8 + 1;
        let mut r = Vec::new();
        r.push(101u8);
        r.push(0);
        r.extend_from_slice(&2u16.to_le_bytes());
        r.push(min);
        r.push(count);
        r.extend_from_slice(&0u16.to_le_bytes());
        self.send(&r)?;
        let rep = self.wait_reply()?;
        let per = rep[1] as usize;
        let extra = u32::from_le_bytes([rep[4], rep[5], rep[6], rep[7]]) as usize * 4;
        let body = &rep[32..32 + extra];
        self.keymap = (0..count as usize)
            .map(|k| {
                (0..per)
                    .map(|j| {
                        let o = (k * per + j) * 4;
                        if o + 4 <= body.len() {
                            u32::from_le_bytes([body[o], body[o + 1], body[o + 2], body[o + 3]])
                        } else {
                            0
                        }
                    })
                    .collect()
            })
            .collect();
        self.min_keycode = min;
        Ok(())
    }

    /// Read until a reply arrives, queueing any events that arrive first.
    fn wait_reply(&mut self) -> std::io::Result<Vec<u8>> {
        loop {
            let mut b = [0u8; 32];
            self.sock.read_exact(&mut b)?;
            match b[0] {
                0 => {
                    return Err(std::io::Error::new(
                        std::io::ErrorKind::Other,
                        format!("X11 error code {} for opcode {}", b[1], b[10]),
                    ))
                }
                1 => {
                    let extra = u32::from_le_bytes([b[4], b[5], b[6], b[7]]) as usize * 4;
                    let mut out = b.to_vec();
                    if extra > 0 {
                        let mut rest = vec![0u8; extra];
                        self.sock.read_exact(&mut rest)?;
                        out.extend_from_slice(&rest);
                    }
                    return Ok(out);
                }
                _ => {
                    if let Some(e) = self.decode_event(&b) {
                        self.pending.push(e);
                    }
                }
            }
        }
    }

    fn keysym(&self, keycode: u8, shift: bool) -> u32 {
        let i = keycode.wrapping_sub(self.min_keycode) as usize;
        match self.keymap.get(i) {
            Some(row) if !row.is_empty() => {
                let k = if shift && row.len() > 1 && row[1] != 0 { row[1] } else { row[0] };
                k
            }
            _ => 0,
        }
    }

    fn decode_event(&self, b: &[u8]) -> Option<Event> {
        let rd16 = |o: usize| i16::from_le_bytes([b[o], b[o + 1]]);
        let ru16 = |o: usize| u16::from_le_bytes([b[o], b[o + 1]]);
        let ru32 = |o: usize| u32::from_le_bytes([b[o], b[o + 1], b[o + 2], b[o + 3]]);
        // The window field sits at a different offset per event type.
        let window = match b[0] & 0x7f {
            2 | 3 | 4 | 5 | 6 => ru32(12), // key/button/motion: "event" window
            12 => ru32(4),                 // Expose
            22 => ru32(8),                 // ConfigureNotify: "window"
            33 => ru32(4),                 // ClientMessage
            _ => 0,
        };
        let kind = self.decode_kind(b, rd16, ru16)?;
        Some(Event { window, kind })
    }

    fn decode_kind(
        &self,
        b: &[u8],
        rd16: impl Fn(usize) -> i16,
        ru16: impl Fn(usize) -> u16,
    ) -> Option<Kind> {
        match b[0] & 0x7f {
            2 => {
                let state = ru16(28);
                Some(Kind::Key(
                    self.keysym(b[1], state & 1 != 0),
                    state & 1 != 0,
                    state & 4 != 0,
                ))
            }
            4 => Some(Kind::ButtonPress(b[1], rd16(24), rd16(26), ru16(28))),
            5 => Some(Kind::ButtonRelease(b[1], rd16(24), rd16(26))),
            6 => Some(Kind::Motion(rd16(24), rd16(26), ru16(28))),
            12 => Some(Kind::Expose),
            22 => Some(Kind::Resize(ru16(20), ru16(22))),
            33 => {
                let data = u32::from_le_bytes([b[12], b[13], b[14], b[15]]);
                if data == self.wm_delete {
                    Some(Kind::Close)
                } else {
                    None
                }
            }
            _ => None,
        }
    }

    /// Block until at least one event is available, then drain everything
    /// pending so a burst of motion events collapses into one repaint.
    pub fn next_events(&mut self) -> std::io::Result<Vec<Event>> {
        let mut out = std::mem::take(&mut self.pending);
        if out.is_empty() {
            let mut b = [0u8; 32];
            self.sock.read_exact(&mut b)?;
            if b[0] > 1 {
                if let Some(e) = self.decode_event(&b) {
                    out.push(e);
                }
            }
        }
        // Non-blocking drain of whatever else is queued.
        self.sock.set_nonblocking(true)?;
        loop {
            let mut b = [0u8; 32];
            match self.sock.read_exact(&mut b) {
                Ok(()) => {
                    if b[0] > 1 {
                        if let Some(e) = self.decode_event(&b) {
                            out.push(e);
                        }
                    }
                }
                Err(_) => break,
            }
        }
        self.sock.set_nonblocking(false)?;
        for e in out.iter() {
            if let Kind::Resize(w, h) = e.kind {
                if e.window == self.wid {
                    self.width = w;
                    self.height = h;
                }
            }
        }
        Ok(out)
    }

    /// Blit an RGB frame to the window.
    ///
    /// Split into row bands: a full frame far exceeds one request, and this
    /// client does not negotiate the BIG-REQUESTS extension.
    pub fn present(&mut self, rgb: &[u8], w: usize, h: usize) -> std::io::Result<()> {
        let (wid, gc) = (self.wid, self.gc);
        self.present_on(wid, gc, rgb, w, h)
    }

    /// Blit to a specific window.
    pub fn present_child(&mut self, wid: u32, rgb: &[u8], w: usize, h: usize) -> std::io::Result<()> {
        let gc = match self.extra.iter().find(|(x, _)| *x == wid) {
            Some((_, g)) => *g,
            None => return Ok(()),
        };
        self.present_on(wid, gc, rgb, w, h)
    }

    fn present_on(&mut self, wid: u32, gc: u32, rgb: &[u8], w: usize, h: usize) -> std::io::Result<()> {
        // Send only the rows that actually changed.
        //
        // A full frame is w*h*4 bytes — 5.2 MB at 1500x860.  Locally that is
        // a memcpy; over a remote display it is the whole cost of a repaint,
        // and a repaint that only moved a cursor was shipping the entire
        // window.  Comparing the previous frame costs about a millisecond.
        let track = wid == self.wid;
        let src_stride = w * 3;
        let mut bands: Vec<(usize, usize, usize, usize)> = Vec::new(); // y0,y1,x0,x1
        if track && self.last_dims == (w, h) && self.last_frame.len() == rgb.len() {
            let mut y = 0usize;
            while y < h {
                let row = &rgb[y * src_stride..(y + 1) * src_stride];
                let prev = &self.last_frame[y * src_stride..(y + 1) * src_stride];
                if row == prev {
                    y += 1;
                    continue;
                }
                // Extend over consecutive changed rows, tracking the union of
                // their changed x ranges.
                let (mut x0, mut x1) = (usize::MAX, 0usize);
                let y_start = y;
                while y < h {
                    let row = &rgb[y * src_stride..(y + 1) * src_stride];
                    let prev = &self.last_frame[y * src_stride..(y + 1) * src_stride];
                    if row == prev {
                        break;
                    }
                    let mut a = 0usize;
                    while a < src_stride && row[a] == prev[a] {
                        a += 1;
                    }
                    let mut b = src_stride;
                    while b > a && row[b - 1] == prev[b - 1] {
                        b -= 1;
                    }
                    x0 = x0.min(a / 3);
                    x1 = x1.max((b + 2) / 3);
                    y += 1;
                }
                bands.push((y_start, y, x0, x1.min(w)));
            }
        } else {
            bands.push((0, h, 0, w));
        }

        if track {
            self.last_dims = (w, h);
            self.last_frame.clear();
            self.last_frame.extend_from_slice(rgb);
        }
        self.last_present_bytes = bands
            .iter()
            .map(|(y0, y1, x0, x1)| (y1 - y0) * (x1 - x0) * 4)
            .sum();

        let mut sent = false;
        for (by0, by1, bx0, bx1) in bands {
            let bw = bx1.saturating_sub(bx0);
            if bw == 0 || by1 <= by0 {
                continue;
            }
            self.put_rect(wid, gc, rgb, w, bx0, by0, bw, by1 - by0)?;
            sent = true;
        }
        self.sock.flush()?;
        if sent {
            self.sync()?;
        }
        Ok(())
    }

    fn put_rect(
        &mut self,
        wid: u32,
        gc: u32,
        rgb: &[u8],
        img_w: usize,
        rx: usize,
        ry: usize,
        w: usize,
        h: usize,
    ) -> std::io::Result<()> {
        let stride = w * 4;
        let max_rows = (((self.max_req_bytes - 32) / stride.max(1)).max(1)).min(h);
        let mut buf = vec![0u8; max_rows * stride];
        let mut y = 0usize;
        while y < h {
            let rows = max_rows.min(h - y);
            for r in 0..rows {
                let src = ((ry + y + r) * img_w + rx) * 3;
                let dst = r * stride;
                for x in 0..w {
                    // Little-endian ZPixmap at depth 24 is B,G,R,unused.
                    buf[dst + x * 4] = rgb[src + x * 3 + 2];
                    buf[dst + x * 4 + 1] = rgb[src + x * 3 + 1];
                    buf[dst + x * 4 + 2] = rgb[src + x * 3];
                    buf[dst + x * 4 + 3] = 0;
                }
            }
            let data = &buf[..rows * stride];
            let len_units = 6 + data.len() / 4;
            let mut r = Vec::with_capacity(24 + data.len());
            r.push(72u8);
            r.push(2); // ZPixmap
            r.extend_from_slice(&(len_units as u16).to_le_bytes());
            r.extend_from_slice(&wid.to_le_bytes());
            r.extend_from_slice(&gc.to_le_bytes());
            r.extend_from_slice(&(w as u16).to_le_bytes());
            r.extend_from_slice(&(rows as u16).to_le_bytes());
            r.extend_from_slice(&(rx as i16).to_le_bytes());
            r.extend_from_slice(&((ry + y) as i16).to_le_bytes());
            r.push(0);
            r.push(self.depth);
            r.extend_from_slice(&0u16.to_le_bytes());
            r.extend_from_slice(data);
            self.send(&r)?;
            y += rows;
        }
        Ok(())
    }
}
