//go:build linux

package wayland

import (
	"context"
	"encoding/binary"
	"errors"
	"fmt"
	"io"
	"log"
	"math"
	"net"
	"os"
	"os/exec"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"chirami/internal/platform"
	"golang.org/x/sys/unix"
)

var debug = os.Getenv("CHIRAMI_DEBUG") != ""

func debugf(format string, args ...any) {
	if debug {
		log.Printf(format, args...)
	}
}

type waylandMsg struct {
	objID  uint32
	opcode uint32
	fd     int
	data   []byte
	err    error
}

type locateReq struct {
	resp chan locateResp
}

type locateResp struct {
	anchor platform.Anchor
	err    error
}

type showReq struct {
	popup platform.Popup
	resp  chan error
}

type hideReq struct {
	resp chan struct{}
}

type outputInfo struct {
	id         uint32
	globalName uint32
	name       string

	physX, physY int32
	physW, physH int32
	outputScale  int32

	xdgOutputID uint32
	hasLogical  bool
	logicalX    int32
	logicalY    int32
	logicalW    int32
	logicalH    int32

	preferredScale float64

	locateSurfID      uint32
	locateLayerSurfID uint32
	locateViewportID  uint32
	locateFracScaleID uint32
	locateW, locateH  int32
	locateMapped      bool
	locateCustomBufID uint32
}

func (o *outputInfo) LogicalWidth() float64 {
	if o.hasLogical && o.logicalW > 0 {
		return float64(o.logicalW)
	}
	s := o.Scale()
	if s <= 0 {
		s = 1.0
	}
	if o.physW > 0 {
		return float64(o.physW) / s
	}
	return 1920.0
}

func (o *outputInfo) LogicalHeight() float64 {
	if o.hasLogical && o.logicalH > 0 {
		return float64(o.logicalH)
	}
	s := o.Scale()
	if s <= 0 {
		s = 1.0
	}
	if o.physH > 0 {
		return float64(o.physH) / s
	}
	return 1080.0
}

func (o *outputInfo) Scale() float64 {
	if o.preferredScale > 0 {
		return o.preferredScale
	}
	if o.outputScale > 0 {
		return float64(o.outputScale)
	}
	return 1.0
}

type poolBufferSlot struct {
	bufID    uint32
	offset   int
	w, h     int
	stride   int
	busy     bool
	released bool
}

type shmPool struct {
	shmID    uint32
	poolID   uint32
	memfd    int
	capacity int
	data     []byte
	slots    [2]poolBufferSlot
}

func newShmPool(shmID uint32) *shmPool {
	return &shmPool{
		shmID: shmID,
		memfd: -1,
	}
}

func (p *shmPool) ensureCapacity(conn *net.UnixConn, capNeeded int, allocID func() uint32, sendMsg func(uint32, uint32, []byte, ...any) error) error {
	if capNeeded <= p.capacity {
		return nil
	}
	aligned := (capNeeded + 0xffff) &^ 0xffff
	if p.memfd < 0 {
		mfd, err := unix.MemfdCreate("chirami-card", unix.MFD_CLOEXEC)
		if err != nil {
			return fmt.Errorf("memfd_create: %w", err)
		}
		if err := unix.Ftruncate(mfd, int64(aligned)); err != nil {
			_ = unix.Close(mfd)
			return fmt.Errorf("ftruncate: %w", err)
		}
		data, err := unix.Mmap(mfd, 0, aligned, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
		if err != nil {
			_ = unix.Close(mfd)
			return fmt.Errorf("mmap: %w", err)
		}
		p.poolID = allocID()
		oob := unix.UnixRights(mfd)
		var cpReq [16]byte
		binary.LittleEndian.PutUint32(cpReq[0:4], p.shmID)
		binary.LittleEndian.PutUint32(cpReq[4:8], (16<<16)|0) // create_pool opcode 0
		binary.LittleEndian.PutUint32(cpReq[8:12], p.poolID)
		binary.LittleEndian.PutUint32(cpReq[12:16], uint32(aligned))
		if _, _, err := conn.WriteMsgUnix(cpReq[:], oob, nil); err != nil {
			_ = unix.Munmap(data)
			_ = unix.Close(mfd)
			return fmt.Errorf("write create_pool: %w", err)
		}
		p.memfd = mfd
		p.capacity = aligned
		p.data = data
		return nil
	}

	if err := unix.Ftruncate(p.memfd, int64(aligned)); err != nil {
		return fmt.Errorf("ftruncate resize: %w", err)
	}
	_ = unix.Munmap(p.data)
	data, err := unix.Mmap(p.memfd, 0, aligned, unix.PROT_READ|unix.PROT_WRITE, unix.MAP_SHARED)
	if err != nil {
		return fmt.Errorf("remmap: %w", err)
	}
	p.data = data
	p.capacity = aligned
	var rReq [12]byte
	binary.LittleEndian.PutUint32(rReq[0:4], p.poolID)
	binary.LittleEndian.PutUint32(rReq[4:8], (12<<16)|2) // wl_shm_pool.resize is opcode 2 (1 is destroy)
	binary.LittleEndian.PutUint32(rReq[8:12], uint32(aligned))
	_, err = conn.Write(rReq[:])
	return err
}

func (p *shmPool) getBuffer(conn *net.UnixConn, w, h int, allocID func() uint32, sendMsg func(uint32, uint32, []byte, ...any) error) (uint32, []byte, int, error) {
	stride := w * 4
	size := stride * h
	capNeeded := size * 2
	if err := p.ensureCapacity(conn, capNeeded, allocID, sendMsg); err != nil {
		return 0, nil, 0, err
	}

	slotIdx := 0
	if p.slots[0].busy && !p.slots[1].busy {
		slotIdx = 1
	}
	slot := &p.slots[slotIdx]
	slot.offset = slotIdx * size

	if slot.bufID != 0 && (slot.w != w || slot.h != h) && !slot.busy {
		var dReq [8]byte
		binary.LittleEndian.PutUint32(dReq[0:4], slot.bufID)
		binary.LittleEndian.PutUint32(dReq[4:8], (8<<16)|0)
		_, _ = conn.Write(dReq[:])
		slot.bufID = 0
	}

	if slot.bufID == 0 {
		slot.bufID = allocID()
		var bReq [32]byte
		binary.LittleEndian.PutUint32(bReq[0:4], p.poolID)
		binary.LittleEndian.PutUint32(bReq[4:8], (32<<16)|0) // create_buffer opcode 0
		binary.LittleEndian.PutUint32(bReq[8:12], slot.bufID)
		binary.LittleEndian.PutUint32(bReq[12:16], uint32(slot.offset))
		binary.LittleEndian.PutUint32(bReq[16:20], uint32(w))
		binary.LittleEndian.PutUint32(bReq[20:24], uint32(h))
		binary.LittleEndian.PutUint32(bReq[24:28], uint32(stride))
		binary.LittleEndian.PutUint32(bReq[28:32], uint32(0)) // ARGB8888
		if _, err := conn.Write(bReq[:]); err != nil {
			return 0, nil, 0, err
		}
		slot.w = w
		slot.h = h
		slot.stride = stride
	}

	slot.busy = true
	slot.released = false
	dst := p.data[slot.offset : slot.offset+size]
	return slot.bufID, dst, stride, nil
}

func (p *shmPool) handleRelease(bufID uint32) {
	if p.slots[0].bufID == bufID {
		p.slots[0].busy = false
		p.slots[0].released = true
	}
	if p.slots[1].bufID == bufID {
		p.slots[1].busy = false
		p.slots[1].released = true
	}
}

func (p *shmPool) adviseIdle() {
	if len(p.data) > 0 {
		_ = unix.Madvise(p.data, unix.MADV_DONTNEED)
	}
}

func (p *shmPool) destroy(conn *net.UnixConn) {
	for i := range p.slots {
		if p.slots[i].bufID != 0 {
			var dReq [8]byte
			binary.LittleEndian.PutUint32(dReq[0:4], p.slots[i].bufID)
			binary.LittleEndian.PutUint32(dReq[4:8], (8<<16)|0)
			_, _ = conn.Write(dReq[:])
			p.slots[i].bufID = 0
		}
	}
	if p.poolID != 0 {
		var dReq [8]byte
		binary.LittleEndian.PutUint32(dReq[0:4], p.poolID)
		binary.LittleEndian.PutUint32(dReq[4:8], (8<<16)|1)
		_, _ = conn.Write(dReq[:])
		p.poolID = 0
	}
	if len(p.data) > 0 {
		_ = unix.Munmap(p.data)
		p.data = nil
	}
	if p.memfd >= 0 {
		_ = unix.Close(p.memfd)
		p.memfd = -1
	}
}

type clipImgReq struct {
	resp chan []byte
}

type backend struct {
	conn       *net.UnixConn
	selections chan string
	events     chan platform.Event

	locateCh chan locateReq
	showCh   chan showReq
	hideCh   chan hideReq

	shots              chan []byte
	shotsOnce          sync.Once
	ctx                context.Context
	clipImgCh          chan clipImgReq
	clipboardNotify    chan struct{}
	lastClipOfferNanos atomic.Int64
}

var (
	_ platform.Backend       = (*backend)(nil)
	_ platform.Screenshotter = (*backend)(nil)
)

func (b *backend) Selections() <-chan string     { return b.selections }
func (b *backend) Events() <-chan platform.Event { return b.events }

func (b *backend) Screenshots() <-chan []byte {
	b.shotsOnce.Do(func() {
		if sock := os.Getenv("NIRI_SOCKET"); sock != "" {
			go b.watchNiri(b.ctx, sock)
		}
	})
	return b.shots
}

// ReadClipboardImage returns the current image/png on the Wayland clipboard,
// using data-control selection (or wl-paste fallback).
func (b *backend) ReadClipboardImage(ctx context.Context) ([]byte, error) {
	resp := make(chan []byte, 1)
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case <-b.ctx.Done():
		return nil, b.ctx.Err()
	case b.clipImgCh <- clipImgReq{resp: resp}:
	}
	select {
	case <-ctx.Done():
		return nil, ctx.Err()
	case <-b.ctx.Done():
		return nil, b.ctx.Err()
	case data := <-resp:
		if len(data) == 0 {
			return nil, errors.New("no image/png on clipboard")
		}
		return data, nil
	}
}

// Pointer is a cheap current pointer position in Anchor space; ok is false where the platform cannot tell (Wayland).
func (b *backend) Pointer() (x, y float64, ok bool) {
	return 0, 0, false
}

func (b *backend) Locate() (platform.Anchor, error) {
	resp := make(chan locateResp, 1)
	b.locateCh <- locateReq{resp: resp}
	r := <-resp
	return r.anchor, r.err
}

func (b *backend) Show(p platform.Popup) error {
	resp := make(chan error, 1)
	b.showCh <- showReq{popup: p, resp: resp}
	return <-resp
}

func (b *backend) Hide() {
	resp := make(chan struct{}, 1)
	b.hideCh <- hideReq{resp: resp}
	<-resp
}

type pendingShowState struct {
	req   showReq
	bufID uint32
	devW  int
	devH  int
	logW  float64
	logH  float64
	scale float64
	hit   platform.Rect
}

// Run connects to Wayland, initializes backend globals, and runs app.
func Run(app func(platform.Backend) error) error {
	runtimeDir := os.Getenv("XDG_RUNTIME_DIR")
	if runtimeDir == "" {
		return errors.New("XDG_RUNTIME_DIR not set")
	}
	disp := os.Getenv("WAYLAND_DISPLAY")
	if disp == "" {
		disp = "wayland-0"
	}
	addr := runtimeDir + "/" + disp
	conn, err := net.DialUnix("unix", nil, &net.UnixAddr{Name: addr, Net: "unix"})
	if err != nil {
		return fmt.Errorf("connect to wayland socket %q: %w", addr, err)
	}
	defer conn.Close()

	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()

	b := &backend{
		conn:            conn,
		selections:      make(chan string, 16),
		events:          make(chan platform.Event, 64),
		locateCh:        make(chan locateReq),
		showCh:          make(chan showReq),
		hideCh:          make(chan hideReq),
		shots:           make(chan []byte, 16),
		ctx:             ctx,
		clipImgCh:       make(chan clipImgReq),
		clipboardNotify: make(chan struct{}, 1),
	}

	var nextID uint32 = 2
	var freeIDs []uint32

	allocID := func() uint32 {
		if n := len(freeIDs); n > 0 {
			id := freeIDs[n-1]
			freeIDs = freeIDs[:n-1]
			return id
		}
		id := nextID
		nextID++
		return id
	}

	sendMsg := func(objID, opcode uint32, oob []byte, args ...any) error {
		var payload []byte
		for _, arg := range args {
			switch v := arg.(type) {
			case uint32:
				var buf [4]byte
				binary.LittleEndian.PutUint32(buf[:], v)
				payload = append(payload, buf[:]...)
			case int32:
				var buf [4]byte
				binary.LittleEndian.PutUint32(buf[:], uint32(v))
				payload = append(payload, buf[:]...)
			case float64:
				fixed := int32(math.Round(v * 256.0))
				var buf [4]byte
				binary.LittleEndian.PutUint32(buf[:], uint32(fixed))
				payload = append(payload, buf[:]...)
			case string:
				payload = append(payload, encodeString(v)...)
			case []byte:
				payload = append(payload, v...)
			}
		}
		totalLen := 8 + len(payload)
		hdr := make([]byte, 8)
		binary.LittleEndian.PutUint32(hdr[0:4], objID)
		binary.LittleEndian.PutUint32(hdr[4:8], uint32((totalLen<<16)|int(opcode)))
		msg := append(hdr, payload...)
		if len(oob) > 0 {
			_, _, err := conn.WriteMsgUnix(msg, oob, nil)
			return err
		}
		_, err := conn.Write(msg)
		return err
	}

	// recent requests, so a protocol error can say what we last sent to the
	// object it names
	type request struct {
		obj, opcode uint32
		args        []any
	}
	var sent [128]request
	var nsent int
	send := func(objID, opcode uint32, args ...any) error {
		sent[nsent%len(sent)] = request{objID, opcode, args}
		nsent++
		return sendMsg(objID, opcode, nil, args...)
	}
	history := func(obj uint32) string {
		var ops []string
		for i := max(0, nsent-len(sent)); i < nsent; i++ {
			s := sent[i%len(sent)]
			mentions := s.obj == obj
			for _, a := range s.args {
				mentions = mentions || a == any(obj)
			}
			if mentions {
				ops = append(ops, fmt.Sprintf("%d.%d%v", s.obj, s.opcode, s.args))
			}
		}
		return strings.Join(ops, " ")
	}

	// Reader goroutine for Wayland wire protocol with persistent framing buffer.
	// The only event carrying a file descriptor is wl_keyboard.keymap.
	var keyboardID atomic.Uint32
	msgCh := make(chan waylandMsg, 128)
	go func() {
		buf := make([]byte, 0, 16384)
		tmp := make([]byte, 8192)
		oob := make([]byte, unix.CmsgSpace(4)*8)
		var pendingFds []int

		for {
			for len(buf) >= 8 {
				objID := binary.LittleEndian.Uint32(buf[0:4])
				opAndSize := binary.LittleEndian.Uint32(buf[4:8])
				opcode := opAndSize & 0xffff
				sz := int(opAndSize >> 16)
				if sz < 8 {
					msgCh <- waylandMsg{err: fmt.Errorf("invalid message length: %d (less than 8)", sz)}
					return
				}
				if len(buf) < sz {
					break
				}
				recFd := -1
				if len(pendingFds) > 0 && objID != 0 && objID == keyboardID.Load() && opcode == 0 {
					recFd = pendingFds[0]
					pendingFds = pendingFds[1:]
				}
				msgData := make([]byte, sz-8)
				copy(msgData, buf[8:sz])
				buf = buf[sz:]
				msgCh <- waylandMsg{objID: objID, opcode: opcode, fd: recFd, data: msgData}
			}

			n, oobn, _, _, err := conn.ReadMsgUnix(tmp, oob)
			if err != nil {
				msgCh <- waylandMsg{err: err}
				return
			}
			if n == 0 && oobn == 0 {
				msgCh <- waylandMsg{err: io.EOF}
				return
			}
			if oobn > 0 {
				scms, err := unix.ParseSocketControlMessage(oob[:oobn])
				if err == nil {
					for _, scm := range scms {
						fds, err := unix.ParseUnixRights(&scm)
						if err == nil {
							pendingFds = append(pendingFds, fds...)
						}
					}
				}
			}
			if n > 0 {
				buf = append(buf, tmp[:n]...)
			}
		}
	}()

	// 1. Get registry (ID 2)
	registryID := allocID()
	if err := send(1, 1, registryID); err != nil {
		return err
	}

	// Initial sync to discover globals
	syncID := allocID()
	if err := send(1, 0, syncID); err != nil {
		return err
	}

	var compName, shmName, seatName, layerShellName, layerShellVersion uint32
	var extDataName, wlrDataName, viewporterName, xdgOutputMgrName, fracScaleMgrName uint32
	initialOutputs := make(map[uint32]struct{})

	for {
		m := <-msgCh
		if m.err != nil {
			return fmt.Errorf("reading wayland globals: %w", m.err)
		}
		if m.objID == registryID && m.opcode == 0 { // global
			if len(m.data) >= 8 {
				name := binary.LittleEndian.Uint32(m.data[0:4])
				iface, _, err := decodeString(m.data, 4)
				if err == nil {
					switch iface {
					case "wl_compositor":
						compName = name
					case "wl_shm":
						shmName = name
					case "wl_seat":
						seatName = name
					case "zwlr_layer_shell_v1":
						layerShellName = name
						if _, next, err := decodeString(m.data, 4); err == nil && len(m.data) >= next+4 {
							layerShellVersion = binary.LittleEndian.Uint32(m.data[next:])
						}
					case "ext_data_control_manager_v1":
						extDataName = name
					case "zwlr_data_control_manager_v1":
						wlrDataName = name
					case "wp_viewporter":
						viewporterName = name
					case "zxdg_output_manager_v1":
						xdgOutputMgrName = name
					case "wp_fractional_scale_manager_v1":
						fracScaleMgrName = name
					case "wl_output":
						initialOutputs[name] = struct{}{}
					}
				}
			}
		}
		if m.objID == syncID && m.opcode == 0 {
			break
		}
	}

	if compName == 0 || shmName == 0 || seatName == 0 || layerShellName == 0 {
		return fmt.Errorf("missing required wayland globals: comp=%d shm=%d seat=%d layer=%d",
			compName, shmName, seatName, layerShellName)
	}

	compID := allocID()
	if err := send(registryID, 0, compName, "wl_compositor", uint32(4), compID); err != nil {
		return err
	}
	shmID := allocID()
	if err := send(registryID, 0, shmName, "wl_shm", uint32(1), shmID); err != nil {
		return err
	}
	seatID := allocID()
	if err := send(registryID, 0, seatName, "wl_seat", uint32(2), seatID); err != nil {
		return err
	}
	// version 4 lets the card take keyboard focus when clicked (on_demand),
	// so Escape and clicking elsewhere can close it
	layerShellVersion = min(max(layerShellVersion, 1), 4)
	layerShellID := allocID()
	if err := send(registryID, 0, layerShellName, "zwlr_layer_shell_v1", layerShellVersion, layerShellID); err != nil {
		return err
	}

	var viewporterID uint32
	if viewporterName != 0 {
		viewporterID = allocID()
		_ = send(registryID, 0, viewporterName, "wp_viewporter", uint32(1), viewporterID)
	}

	var xdgOutputMgrID uint32
	if xdgOutputMgrName != 0 {
		xdgOutputMgrID = allocID()
		_ = send(registryID, 0, xdgOutputMgrName, "zxdg_output_manager_v1", uint32(3), xdgOutputMgrID)
	}

	var fracScaleMgrID uint32
	if fracScaleMgrName != 0 {
		fracScaleMgrID = allocID()
		_ = send(registryID, 0, fracScaleMgrName, "wp_fractional_scale_manager_v1", uint32(1), fracScaleMgrID)
	}

	// Track per-output state
	boundOutputs := make(map[uint32]*outputInfo)      // keyed by outID
	boundXdgOutputs := make(map[uint32]*outputInfo)   // keyed by xdgOutputID
	boundFracScales := make(map[uint32]*outputInfo)   // keyed by fracScaleID
	boundLocateSurfs := make(map[uint32]*outputInfo)  // keyed by locateSurfID
	boundLocateLayers := make(map[uint32]*outputInfo) // keyed by locateLayerSurfID

	// 1x1 transparent shm buffer for locate overlays (when viewporter is present)
	locate1x1Memfd, err := unix.MemfdCreate("chirami-locate-1x1", unix.MFD_CLOEXEC)
	if err != nil {
		return err
	}
	defer unix.Close(locate1x1Memfd)
	_ = unix.Ftruncate(locate1x1Memfd, 4)
	locate1x1PoolID := allocID()
	oob := unix.UnixRights(locate1x1Memfd)
	var cpReq [16]byte
	binary.LittleEndian.PutUint32(cpReq[0:4], shmID)
	binary.LittleEndian.PutUint32(cpReq[4:8], (16<<16)|0)
	binary.LittleEndian.PutUint32(cpReq[8:12], locate1x1PoolID)
	binary.LittleEndian.PutUint32(cpReq[12:16], 4)
	_, _, _ = conn.WriteMsgUnix(cpReq[:], oob, nil)
	locate1x1BufID := allocID()
	_ = send(locate1x1PoolID, 0, locate1x1BufID, int32(0), int32(1), int32(1), int32(4), uint32(0))

	// Helper to bind an output and set up its locate layer surface
	bindOutput := func(globalName uint32) *outputInfo {
		outID := allocID()
		_ = send(registryID, 0, globalName, "wl_output", uint32(2), outID)
		out := &outputInfo{
			id:          outID,
			globalName:  globalName,
			outputScale: 1,
			physW:       1920,
			physH:       1080,
		}
		boundOutputs[outID] = out

		if xdgOutputMgrID != 0 {
			xdgOutID := allocID()
			_ = send(xdgOutputMgrID, 1, xdgOutID, outID) // get_xdg_output opcode 1
			out.xdgOutputID = xdgOutID
			boundXdgOutputs[xdgOutID] = out
		}

		// Locate surfaces are created on-demand during Locate() and unmapped/destroyed immediately after

		return out
	}

	for name := range initialOutputs {
		bindOutput(name)
	}

	// Data control binding: prefer ext_data_control_manager_v1, fall back to zwlr_data_control_manager_v1 v2
	var dataDeviceID uint32
	if extDataName != 0 {
		extDataID := allocID()
		if err := send(registryID, 0, extDataName, "ext_data_control_manager_v1", uint32(1), extDataID); err == nil {
			dataDeviceID = allocID()
			_ = send(extDataID, 1, dataDeviceID, seatID)
		}
	} else if wlrDataName != 0 {
		wlrDataID := allocID()
		if err := send(registryID, 0, wlrDataName, "zwlr_data_control_manager_v1", uint32(2), wlrDataID); err == nil {
			dataDeviceID = allocID()
			_ = send(wlrDataID, 1, dataDeviceID, seatID)
		}
	}

	// Fallback polling if no data control protocol is supported
	if dataDeviceID == 0 {
		go func() {
			var lastPolled string
			ticker := time.NewTicker(300 * time.Millisecond)
			defer ticker.Stop()
			for {
				select {
				case <-ctx.Done():
					return
				case <-ticker.C:
					cmd := exec.CommandContext(ctx, "wl-paste", "--primary", "--no-newline")
					out, err := cmd.Output()
					if err == nil && len(out) > 0 {
						s := string(out)
						if s != lastPolled {
							lastPolled = s
							select {
							case b.selections <- s:
							default:
							}
						}
					}
				}
			}
		}()
	}

	var pointerID uint32

	// seatEvent asks for the pointer once the seat says it has one; the
	// capabilities event usually arrives during the initial sync below
	seatEvent := func(m waylandMsg) bool {
		if m.objID != seatID || m.opcode != 0 || len(m.data) < 4 {
			return false
		}
		caps := binary.LittleEndian.Uint32(m.data[0:4])
		if caps&1 != 0 && pointerID == 0 {
			pointerID = allocID()
			_ = send(seatID, 0, pointerID) // seat.get_pointer
		}
		if caps&2 != 0 && keyboardID.Load() == 0 && layerShellVersion >= 4 {
			id := allocID()
			keyboardID.Store(id)
			_ = send(seatID, 1, id) // seat.get_keyboard
		}
		return true
	}

	// keymap closes the keymap's descriptor: the card only needs Escape,
	// whose evdev code needs no keymap
	keymap := func(m waylandMsg) bool {
		if m.objID == 0 || m.objID != keyboardID.Load() || m.opcode != 0 {
			return false
		}
		if m.fd >= 0 {
			_ = unix.Close(m.fd)
		}
		return true
	}

	offers := make(map[uint32][]string)
	var primaryOffer, clipboardOffer uint32

	handleDataControl := func(m waylandMsg) bool {
		if dataDeviceID != 0 && m.objID == dataDeviceID {
			if m.opcode == 0 { // data_offer(new_id)
				offID := binary.LittleEndian.Uint32(m.data[0:4])
				offers[offID] = nil
			} else if m.opcode == 1 { // selection(id)
				offID := binary.LittleEndian.Uint32(m.data[0:4])
				clipboardOffer = offID
				b.lastClipOfferNanos.Store(time.Now().UnixNano())
				if offID != 0 && offerHasImage(offers[offID]) {
					select {
					case b.clipboardNotify <- struct{}{}:
					default:
					}
				}
				for oid := range offers {
					if oid != primaryOffer && oid != clipboardOffer {
						_ = send(oid, 1)
						delete(offers, oid)
					}
				}
			} else if m.opcode == 3 { // primary_selection(id)
				offID := binary.LittleEndian.Uint32(m.data[0:4])
				primaryOffer = offID
				if offID != 0 {
					mimes := offers[offID]
					pick := ""
					for _, cand := range []string{"text/plain;charset=utf-8", "UTF8_STRING", "text/plain"} {
						for _, m := range mimes {
							if strings.EqualFold(m, cand) {
								pick = m
								break
							}
						}
						if pick != "" {
							break
						}
					}
					if pick == "" && len(mimes) > 0 {
						for _, m := range mimes {
							if strings.HasPrefix(m, "text/") {
								pick = m
								break
							}
						}
					}
					if pick != "" {
						pr, pw, err := os.Pipe()
						if err == nil {
							strEnc := encodeString(pick)
							recLen := 8 + len(strEnc)
							recReq := make([]byte, recLen)
							binary.LittleEndian.PutUint32(recReq[0:4], offID)
							binary.LittleEndian.PutUint32(recReq[4:8], uint32((recLen<<16)|0))
							copy(recReq[8:], strEnc)
							oob := unix.UnixRights(int(pw.Fd()))
							_, _, _ = conn.WriteMsgUnix(recReq, oob, nil)
							_ = pw.Close()

							go func(r *os.File) {
								defer r.Close()
								_ = r.SetReadDeadline(time.Now().Add(500 * time.Millisecond))
								data, err := io.ReadAll(io.LimitReader(r, 65536))
								if err == nil && len(data) > 0 {
									text := string(data)
									select {
									case b.selections <- text:
									default:
									}
								}
							}(pr)
						}
					}
				}
				for oid := range offers {
					if oid != primaryOffer && oid != clipboardOffer {
						_ = send(oid, 1)
						delete(offers, oid)
					}
				}
			}
			return true
		}

		if _, exists := offers[m.objID]; exists && m.opcode == 0 { // offer(mime_type)
			mime, _, err := decodeString(m.data, 0)
			if err == nil && mime != "" {
				offers[m.objID] = append(offers[m.objID], mime)
				if m.objID == clipboardOffer && strings.EqualFold(mime, "image/png") {
					select {
					case b.clipboardNotify <- struct{}{}:
					default:
					}
				}
			}
			return true
		}
		return false
	}

	// Sync after binding initial globals
	syncID = allocID()
	if err := send(1, 0, syncID); err != nil {
		return err
	}

	for {
		m := <-msgCh
		if m.err != nil {
			return m.err
		}
		if handleDataControl(m) || seatEvent(m) || keymap(m) {
			continue
		}
		if m.objID == 1 && m.opcode == 0 {
			errStr, _, _ := decodeString(m.data, 8)
			return fmt.Errorf("display error: %s", errStr)
		}
		if m.objID == 1 && m.opcode == 1 && len(m.data) >= 4 {
			delID := binary.LittleEndian.Uint32(m.data[0:4])
			if delID > 0 {
				freeIDs = append(freeIDs, delID)
			}
		}
		if out, ok := boundOutputs[m.objID]; ok {
			switch m.opcode {
			case 0:
				if len(m.data) >= 8 {
					out.physX = int32(binary.LittleEndian.Uint32(m.data[0:4]))
					out.physY = int32(binary.LittleEndian.Uint32(m.data[4:8]))
				}
			case 1:
				if len(m.data) >= 12 {
					flags := binary.LittleEndian.Uint32(m.data[0:4])
					if (flags & 1) != 0 {
						out.physW = int32(binary.LittleEndian.Uint32(m.data[4:8]))
						out.physH = int32(binary.LittleEndian.Uint32(m.data[8:12]))
					}
				}
			case 3:
				if len(m.data) >= 4 {
					out.outputScale = int32(binary.LittleEndian.Uint32(m.data[0:4]))
				}
			case 4:
				name, _, _ := decodeString(m.data, 0)
				if name != "" {
					out.name = name
				}
			}
		}
		if out, ok := boundXdgOutputs[m.objID]; ok {
			switch m.opcode {
			case 0:
				if len(m.data) >= 8 {
					out.logicalX = int32(binary.LittleEndian.Uint32(m.data[0:4]))
					out.logicalY = int32(binary.LittleEndian.Uint32(m.data[4:8]))
				}
			case 1:
				if len(m.data) >= 8 {
					out.logicalW = int32(binary.LittleEndian.Uint32(m.data[0:4]))
					out.logicalH = int32(binary.LittleEndian.Uint32(m.data[4:8]))
					out.hasLogical = true
				}
			case 3:
				name, _, _ := decodeString(m.data, 0)
				if name != "" {
					out.name = name
				}
			}
		}
		if out, ok := boundFracScales[m.objID]; ok {
			if m.opcode == 0 && len(m.data) >= 4 {
				scaleVal := binary.LittleEndian.Uint32(m.data[0:4])
				if scaleVal > 0 {
					out.preferredScale = float64(scaleVal) / 120.0
				}
			}
		}

		if m.objID == syncID && m.opcode == 0 {
			break
		}
	}

	// State inside event loop
	var (
		lastKnownOutput *outputInfo
		lastKnownX      = 960.0
		lastKnownY      = 540.0
		showing         = false
		pointerInPopup  = false
		keyboardOnPopup = false // the card has keyboard focus: it was clicked
		pointerX        = 0.0
		pointerY        = 0.0

		// Popup surface state
		popupSurfID      uint32
		popupLayerSurfID uint32
		popupViewportID  uint32
		popupOutID       uint32
		pendingShow      *pendingShowState

		// Offers map is managed by handleDataControl

		// Locate synchronization
		locateWaiters []chan locateResp
		locateStart   time.Time
		locateTimer   *time.Timer
		locateTimerCh <-chan time.Time
	)

	// on_demand (2, layer shell v4) gives the card keyboard focus only when
	// it is clicked; older versions get none
	popupKeyboard := uint32(0)
	if layerShellVersion >= 4 {
		popupKeyboard = 2
	}

	shm := newShmPool(shmID)
	defer shm.destroy(conn)

	finishLocate := func(targetOut *outputInfo, x, y float64) {
		dur := time.Since(locateStart)
		if locateTimer != nil {
			if !locateTimer.Stop() {
				select {
				case <-locateTimer.C:
				default:
				}
			}
			locateTimer = nil
			locateTimerCh = nil
		}

		if targetOut == nil {
			for _, o := range boundOutputs {
				targetOut = o
				break
			}
		}
		if targetOut == nil {
			targetOut = &outputInfo{physW: 1920, physH: 1080, outputScale: 1}
		}

		lastKnownOutput = targetOut
		lastKnownX = x
		lastKnownY = y

		logW := targetOut.LogicalWidth()
		logH := targetOut.LogicalHeight()
		scale := targetOut.Scale()

		debugf("[wayland] locate complete in %v: output=%q pos=(%.1f, %.1f) screen=(0, 0, %.0f, %.0f) scale=%.1f (unmapping all %d locate overlays)",
			dur, targetOut.name, x, y, logW, logH, scale, len(boundOutputs))

		// Unmap and destroy ALL locate surfaces immediately
		// This ensures direct scanout for fullscreen games is never blocked and zero compositor overhead while idle
		for _, out := range boundOutputs {
			if out.locateSurfID != 0 {
				_ = send(out.locateSurfID, 1, uint32(0), int32(0), int32(0)) // attach nil buffer to unmap
				_ = send(out.locateSurfID, 6)                                // commit
				if out.locateLayerSurfID != 0 {
					_ = send(out.locateLayerSurfID, 7) // destroy layer surface
					delete(boundLocateLayers, out.locateLayerSurfID)
					out.locateLayerSurfID = 0
				}
				if out.locateViewportID != 0 {
					_ = send(out.locateViewportID, 0) // destroy wp_viewport
				}
				_ = send(out.locateSurfID, 0) // destroy wl_surface
				delete(boundLocateSurfs, out.locateSurfID)
				out.locateSurfID = 0
				out.locateViewportID = 0
				out.locateMapped = false
			}
		}

		anc := platform.Anchor{
			X:      x,
			Y:      y,
			Screen: platform.Rect{X: 0, Y: 0, W: logW, H: logH},
			Scale:  scale,
		}

		for _, w := range locateWaiters {
			w <- locateResp{anchor: anc, err: nil}
		}
		locateWaiters = nil
	}

	appErrCh := make(chan error, 1)
	go func() {
		appErrCh <- app(b)
	}()

	for {
		select {
		case err := <-appErrCh:
			return err

		case <-locateTimerCh:
			locateTimer = nil
			locateTimerCh = nil
			fallbackOut := lastKnownOutput
			if fallbackOut == nil {
				for _, o := range boundOutputs {
					fallbackOut = o
					break
				}
			}
			fbX := lastKnownX
			fbY := lastKnownY
			if fallbackOut != nil && (fbX <= 0 && fbY <= 0) {
				fbX = fallbackOut.LogicalWidth() / 2.0
				fbY = fallbackOut.LogicalHeight() / 2.0
			}
			finishLocate(fallbackOut, fbX, fbY)

		case req := <-b.locateCh:
			showing = false
			pointerInPopup = false
			locateWaiters = append(locateWaiters, req.resp)

			if len(locateWaiters) == 1 {
				locateStart = time.Now()
				// Map locate overlays across all outputs
				for _, out := range boundOutputs {
					surfID := allocID()
					_ = send(compID, 0, surfID)
					out.locateSurfID = surfID
					boundLocateSurfs[surfID] = out

					if viewporterID != 0 {
						vpID := allocID()
						_ = send(viewporterID, 1, vpID, surfID)
						out.locateViewportID = vpID
					}

					layerSurfID := allocID()
					_ = send(layerShellID, 0, layerSurfID, surfID, out.id, uint32(3), "chirami-locate")
					out.locateLayerSurfID = layerSurfID
					boundLocateLayers[layerSurfID] = out

					_ = send(layerSurfID, 0, uint32(0), uint32(0))
					_ = send(layerSurfID, 1, uint32(15))
					_ = send(layerSurfID, 3, int32(0), int32(0), int32(0), int32(0))
					_ = send(layerSurfID, 2, uint32(0xffffffff))
					_ = send(layerSurfID, 4, uint32(0))
					_ = send(surfID, 6) // initial commit without buffer
				}

				if locateTimer == nil {
					locateTimer = time.NewTimer(150 * time.Millisecond)
					locateTimerCh = locateTimer.C
				}
			}

		case req := <-b.showCh:
			p := req.popup
			targetOut := lastKnownOutput
			if targetOut == nil {
				for _, o := range boundOutputs {
					targetOut = o
					break
				}
			}
			if targetOut == nil {
				targetOut = &outputInfo{physW: 1920, physH: 1080, outputScale: 1}
			}

			scale := targetOut.Scale()
			if scale <= 0 {
				scale = 1.0
			}
			bounds := p.Image.Bounds()
			devW := bounds.Dx()
			devH := bounds.Dy()
			logW := float64(devW) / scale
			logH := float64(devH) / scale

			bufID, dst, stride, err := shm.getBuffer(conn, devW, devH, allocID, sendMsg)
			if err != nil {
				req.resp <- err
				continue
			}

			// Copy pixels converting premultiplied RGBA to BGRA (PixOffset-correct)
			for y := 0; y < devH; y++ {
				for x := 0; x < devW; x++ {
					srcIdx := p.Image.PixOffset(bounds.Min.X+x, bounds.Min.Y+y)
					dstIdx := y*stride + x*4
					r := p.Image.Pix[srcIdx+0]
					g := p.Image.Pix[srcIdx+1]
					b := p.Image.Pix[srcIdx+2]
					a := p.Image.Pix[srcIdx+3]
					dst[dstIdx+0] = b
					dst[dstIdx+1] = g
					dst[dstIdx+2] = r
					dst[dstIdx+3] = a
				}
			}

			// If popup already showing on this same output, update in-place
			if popupSurfID != 0 && popupOutID == targetOut.id {
				_ = send(popupLayerSurfID, 0, uint32(math.Round(logW)), uint32(math.Round(logH)))
				_ = send(popupLayerSurfID, 1, uint32(5))
				_ = send(popupLayerSurfID, 3, int32(math.Round(p.Y)), int32(0), int32(0), int32(math.Round(p.X)))
				if popupViewportID != 0 {
					_ = send(popupViewportID, 2, int32(math.Round(logW)), int32(math.Round(logH)))
				} else {
					_ = send(popupSurfID, 8, int32(math.Round(scale)))
				}
				regID := allocID()
				_ = send(compID, 1, regID)
				_ = send(regID, 1, int32(p.Hit.X), int32(p.Hit.Y), int32(p.Hit.W), int32(p.Hit.H))
				_ = send(popupSurfID, 5, regID)
				_ = send(regID, 0)
				_ = send(popupSurfID, 1, bufID, int32(0), int32(0))
				_ = send(popupSurfID, 9, int32(0), int32(0), int32(devW), int32(devH))
				_ = send(popupSurfID, 6)
				showing = true
				req.resp <- nil
				continue
			}

			// Popup was hidden or on a different output: destroy old surface if any
			if popupSurfID != 0 {
				keyboardOnPopup = false
				_ = send(popupLayerSurfID, 7) // zwlr_layer_surface_v1.destroy
				if popupViewportID != 0 {
					_ = send(popupViewportID, 0) // wp_viewport.destroy
				}
				_ = send(popupSurfID, 0) // wl_surface.destroy
				popupSurfID = 0
				popupLayerSurfID = 0
				popupViewportID = 0
			}

			// Create popup layer surface on target output
			popupSurfID = allocID()
			_ = send(compID, 0, popupSurfID)
			popupOutID = targetOut.id

			if viewporterID != 0 {
				popupViewportID = allocID()
				_ = send(viewporterID, 1, popupViewportID, popupSurfID)
			}

			popupLayerSurfID = allocID()
			_ = send(layerShellID, 0, popupLayerSurfID, popupSurfID, targetOut.id, uint32(3), "chirami-popup")
			_ = send(popupLayerSurfID, 0, uint32(math.Round(logW)), uint32(math.Round(logH)))
			_ = send(popupLayerSurfID, 1, uint32(5)) // anchor top|left
			_ = send(popupLayerSurfID, 3, int32(math.Round(p.Y)), int32(0), int32(0), int32(math.Round(p.X)))
			_ = send(popupLayerSurfID, 2, uint32(0xffffffff)) // exclusive -1
			_ = send(popupLayerSurfID, 4, popupKeyboard)      // focus when clicked
			_ = send(popupSurfID, 6)                          // initial commit without buffer

			pendingShow = &pendingShowState{
				req:   req,
				bufID: bufID,
				devW:  devW,
				devH:  devH,
				logW:  logW,
				logH:  logH,
				scale: scale,
				hit:   p.Hit,
			}

		case req := <-b.hideCh:
			showing = false
			pointerInPopup = false
			keyboardOnPopup = false
			if pendingShow != nil {
				pendingShow.req.resp <- errors.New("popup hidden before configure")
				pendingShow = nil
			}
			if popupSurfID != 0 {
				_ = send(popupLayerSurfID, 7) // destroy layer surface
				if popupViewportID != 0 {
					_ = send(popupViewportID, 0) // destroy wp_viewport
				}
				_ = send(popupSurfID, 0) // destroy wl_surface
				popupSurfID = 0
				popupLayerSurfID = 0
				popupViewportID = 0
				popupOutID = 0
			}
			shm.adviseIdle()
			req.resp <- struct{}{}

		case req := <-b.clipImgCh:
			if clipboardOffer != 0 && offerHasImage(offers[clipboardOffer]) {
				pr, pw, err := os.Pipe()
				if err != nil {
					req.resp <- nil
					continue
				}
				strEnc := encodeString("image/png")
				recLen := 8 + len(strEnc)
				recReq := make([]byte, recLen)
				binary.LittleEndian.PutUint32(recReq[0:4], clipboardOffer)
				binary.LittleEndian.PutUint32(recReq[4:8], uint32((recLen<<16)|0))
				copy(recReq[8:], strEnc)
				oob := unix.UnixRights(int(pw.Fd()))
				_, _, _ = conn.WriteMsgUnix(recReq, oob, nil)
				_ = pw.Close()

				go func(r *os.File, resp chan []byte) {
					defer r.Close()
					_ = r.SetReadDeadline(time.Now().Add(2 * time.Second))
					data, err := io.ReadAll(io.LimitReader(r, 64*1024*1024))
					if err == nil && len(data) > 0 {
						resp <- data
					} else {
						resp <- nil
					}
				}(pr, req.resp)
			} else if dataDeviceID == 0 {
				go func(resp chan []byte) {
					cmd := exec.CommandContext(b.ctx, "wl-paste", "--type", "image/png", "--no-newline")
					out, err := cmd.Output()
					if err == nil && len(out) > 0 {
						resp <- out
					} else {
						resp <- nil
					}
				}(req.resp)
			} else {
				req.resp <- nil
			}

		case m := <-msgCh:
			if m.err != nil {
				return m.err
			}

			// Display error
			if m.objID == 1 && m.opcode == 0 {
				failedObj := binary.LittleEndian.Uint32(m.data[0:4])
				code := binary.LittleEndian.Uint32(m.data[4:8])
				errStr, _, _ := decodeString(m.data, 8)
				// "invalid object N" names an object we sent but the compositor
				// no longer knows, usually as an argument
				var bad uint32
				if _, err := fmt.Sscanf(errStr, "invalid object %d", &bad); err != nil {
					bad = failedObj
				}
				if os.Getenv("CHIRAMI_WL_TRACE") != "" {
					for i := max(0, nsent-len(sent)); i < nsent; i++ {
						s := sent[i%len(sent)]
						log.Printf("sent %d.%d%v", s.obj, s.opcode, s.args)
					}
				}
				return fmt.Errorf("wayland display error (obj %d, code %d): %s (recent requests involving %d: %s)", failedObj, code, errStr, bad, history(bad))
			}

			// Finding 2.1: wl_display.delete_id
			if m.objID == 1 && m.opcode == 1 {
				if len(m.data) >= 4 {
					delID := binary.LittleEndian.Uint32(m.data[0:4])
					if delID > 0 {
						freeIDs = append(freeIDs, delID)
					}
				}
				continue
			}

			// Finding 2.2: runtime wl_registry events (global / global_remove)
			if m.objID == registryID {
				if m.opcode == 0 { // global
					name := binary.LittleEndian.Uint32(m.data[0:4])
					iface, _, err := decodeString(m.data, 4)
					if err == nil && iface == "wl_output" {
						bindOutput(name)
					}
				} else if m.opcode == 1 { // global_remove
					name := binary.LittleEndian.Uint32(m.data[0:4])
					for outID, out := range boundOutputs {
						if out.globalName == name {
							if out.locateLayerSurfID != 0 {
								_ = send(out.locateLayerSurfID, 7)
								_ = send(out.locateSurfID, 0)
							}
							delete(boundOutputs, outID)
							if out.xdgOutputID != 0 {
								delete(boundXdgOutputs, out.xdgOutputID)
							}
							if out.locateFracScaleID != 0 {
								delete(boundFracScales, out.locateFracScaleID)
							}
							delete(boundLocateSurfs, out.locateSurfID)
							delete(boundLocateLayers, out.locateLayerSurfID)
							if lastKnownOutput == out {
								lastKnownOutput = nil
							}
							break
						}
					}
				}
				continue
			}

			// Output events
			if out, ok := boundOutputs[m.objID]; ok {
				switch m.opcode {
				case 0: // geometry
					if len(m.data) >= 8 {
						out.physX = int32(binary.LittleEndian.Uint32(m.data[0:4]))
						out.physY = int32(binary.LittleEndian.Uint32(m.data[4:8]))
					}
				case 1: // mode
					if len(m.data) >= 12 {
						flags := binary.LittleEndian.Uint32(m.data[0:4])
						if (flags & 1) != 0 {
							out.physW = int32(binary.LittleEndian.Uint32(m.data[4:8]))
							out.physH = int32(binary.LittleEndian.Uint32(m.data[8:12]))
						}
					}
				case 3: // scale
					if len(m.data) >= 4 {
						out.outputScale = int32(binary.LittleEndian.Uint32(m.data[0:4]))
					}
				case 4: // name
					name, _, _ := decodeString(m.data, 0)
					if name != "" {
						out.name = name
					}
				}
				continue
			}

			// XDG output events
			if out, ok := boundXdgOutputs[m.objID]; ok {
				switch m.opcode {
				case 0: // logical_position
					if len(m.data) >= 8 {
						out.logicalX = int32(binary.LittleEndian.Uint32(m.data[0:4]))
						out.logicalY = int32(binary.LittleEndian.Uint32(m.data[4:8]))
					}
				case 1: // logical_size
					if len(m.data) >= 8 {
						out.logicalW = int32(binary.LittleEndian.Uint32(m.data[0:4]))
						out.logicalH = int32(binary.LittleEndian.Uint32(m.data[4:8]))
						out.hasLogical = true
					}
				case 3: // name
					name, _, _ := decodeString(m.data, 0)
					if name != "" {
						out.name = name
					}
				}
				continue
			}

			// Fractional scale events
			if out, ok := boundFracScales[m.objID]; ok {
				if m.opcode == 0 && len(m.data) >= 4 { // preferred_scale
					scaleVal := binary.LittleEndian.Uint32(m.data[0:4])
					if scaleVal > 0 {
						out.preferredScale = float64(scaleVal) / 120.0
					}
				}
				continue
			}

			// Locate layer surface configure events (Finding 1 & 6.2)
			if out, ok := boundLocateLayers[m.objID]; ok && m.opcode == 0 {
				serial := binary.LittleEndian.Uint32(m.data[0:4])
				w := binary.LittleEndian.Uint32(m.data[4:8])
				h := binary.LittleEndian.Uint32(m.data[8:12])
				out.locateW = int32(w)
				out.locateH = int32(h)

				_ = send(out.locateLayerSurfID, 6, serial) // ack_configure

				if len(locateWaiters) > 0 {
					if out.locateViewportID != 0 {
						_ = send(out.locateViewportID, 2, int32(w), int32(h))
						_ = send(out.locateSurfID, 1, locate1x1BufID, int32(0), int32(0))
						_ = send(out.locateSurfID, 9, int32(0), int32(0), int32(1), int32(1))
					} else {
						if out.locateCustomBufID == 0 {
							stride := int(w) * 4
							bufSize := stride * int(h)
							tMfd, err := unix.MemfdCreate("chirami-locate-trans", unix.MFD_CLOEXEC)
							if err == nil {
								_ = unix.Ftruncate(tMfd, int64(bufSize))
								tPoolID := allocID()
								tOob := unix.UnixRights(tMfd)
								var tpReq [16]byte
								binary.LittleEndian.PutUint32(tpReq[0:4], shmID)
								binary.LittleEndian.PutUint32(tpReq[4:8], (16<<16)|0)
								binary.LittleEndian.PutUint32(tpReq[8:12], tPoolID)
								binary.LittleEndian.PutUint32(tpReq[12:16], uint32(bufSize))
								_, _, _ = conn.WriteMsgUnix(tpReq[:], tOob, nil)
								out.locateCustomBufID = allocID()
								_ = send(tPoolID, 0, out.locateCustomBufID, int32(0), int32(w), int32(h), int32(stride), uint32(0))
								_ = unix.Close(tMfd)
							}
						}
						if out.locateCustomBufID != 0 {
							_ = send(out.locateSurfID, 1, out.locateCustomBufID, int32(0), int32(0))
							_ = send(out.locateSurfID, 9, int32(0), int32(0), int32(w), int32(h))
						}
					}

					// Fullscreen input region to capture pointer
					regID := allocID()
					_ = send(compID, 1, regID)
					_ = send(regID, 1, int32(0), int32(0), int32(w), int32(h))
					_ = send(out.locateSurfID, 5, regID)
					_ = send(regID, 0)
					_ = send(out.locateSurfID, 6) // commit to map with input region
					out.locateMapped = true
				}
				continue
			}

			// Popup layer surface configure events (Finding 6.3)
			if popupLayerSurfID != 0 && m.objID == popupLayerSurfID && m.opcode == 0 {
				serial := binary.LittleEndian.Uint32(m.data[0:4])
				_ = send(popupLayerSurfID, 6, serial) // ack_configure

				if pendingShow != nil {
					ps := pendingShow
					pendingShow = nil

					if popupViewportID != 0 {
						_ = send(popupViewportID, 2, int32(math.Round(ps.logW)), int32(math.Round(ps.logH)))
					} else {
						_ = send(popupSurfID, 8, int32(math.Round(ps.scale)))
					}

					regID := allocID()
					_ = send(compID, 1, regID)
					_ = send(regID, 1, int32(ps.hit.X), int32(ps.hit.Y), int32(ps.hit.W), int32(ps.hit.H))
					_ = send(popupSurfID, 5, regID)
					_ = send(regID, 0)

					_ = send(popupSurfID, 1, ps.bufID, int32(0), int32(0))
					_ = send(popupSurfID, 9, int32(0), int32(0), int32(ps.devW), int32(ps.devH))
					_ = send(popupSurfID, 6)

					showing = true
					pointerInPopup = false
					ps.req.resp <- nil
				}
				continue
			}

			// Buffer release events (Finding 3.1)
			if m.opcode == 0 {
				shm.handleRelease(m.objID)
			}

			if seatEvent(m) || keymap(m) {
				continue
			}

			// Keyboard events: once clicked, the card holds keyboard focus
			// (on_demand), so Escape reaches it. Focus moving elsewhere is
			// ignored: clicking outside leaves the card up.
			if kb := keyboardID.Load(); kb != 0 && m.objID == kb {
				dismiss := false
				switch m.opcode {
				case 1: // enter(serial, surface, keys)
					if len(m.data) >= 8 && binary.LittleEndian.Uint32(m.data[4:8]) == popupSurfID && showing {
						keyboardOnPopup = true
					}
				case 2: // leave(serial, surface)
					keyboardOnPopup = false
				case 3: // key(serial, time, key, state)
					const esc = 1 // evdev KEY_ESC
					dismiss = len(m.data) >= 16 && keyboardOnPopup && showing &&
						binary.LittleEndian.Uint32(m.data[8:12]) == esc && binary.LittleEndian.Uint32(m.data[12:16]) == 1
				}
				if dismiss {
					select {
					case b.events <- platform.Event{Kind: platform.Dismiss}:
					default:
					}
				}
				continue
			}

			// Data control events
			if handleDataControl(m) {
				continue
			}

			// Pointer events
			if m.objID == pointerID {
				switch m.opcode {
				case 0: // enter(serial, surface, sx, sy)
					if len(m.data) >= 16 {
						surf := binary.LittleEndian.Uint32(m.data[4:8])
						rawX := int32(binary.LittleEndian.Uint32(m.data[8:12]))
						rawY := int32(binary.LittleEndian.Uint32(m.data[12:16]))
						fx := float64(rawX) / 256.0
						fy := float64(rawY) / 256.0

						if out, ok := boundLocateSurfs[surf]; ok {
							if len(locateWaiters) > 0 {
								finishLocate(out, fx, fy)
							}
						} else if surf == popupSurfID && showing {
							pointerInPopup = true
							pointerX = fx
							pointerY = fy
							select {
							case b.events <- platform.Event{Kind: platform.Motion, X: fx, Y: fy}:
							default:
							}
						}
					}

				case 1: // leave(serial, surface)
					if len(m.data) >= 8 {
						surf := binary.LittleEndian.Uint32(m.data[4:8])
						if surf == popupSurfID && showing && pointerInPopup {
							pointerInPopup = false
							select {
							case b.events <- platform.Event{Kind: platform.Leave, X: pointerX, Y: pointerY}:
							default:
							}
						}
					}

				case 2: // motion(time, sx, sy)
					if len(m.data) >= 12 {
						rawX := int32(binary.LittleEndian.Uint32(m.data[4:8]))
						rawY := int32(binary.LittleEndian.Uint32(m.data[8:12]))
						fx := float64(rawX) / 256.0
						fy := float64(rawY) / 256.0

						if showing && pointerInPopup {
							pointerX = fx
							pointerY = fy
							select {
							case b.events <- platform.Event{Kind: platform.Motion, X: fx, Y: fy}:
							default:
							}
						}
					}

				case 3: // button(serial, time, button, state)
					if len(m.data) >= 16 {
						btn := binary.LittleEndian.Uint32(m.data[8:12])
						state := binary.LittleEndian.Uint32(m.data[12:16])
						if showing && pointerInPopup && btn == 272 && state == 0 {
							select {
							case b.events <- platform.Event{Kind: platform.Click, X: pointerX, Y: pointerY}:
							default:
							}
						}
					}
				}
			}
		}
	}
}

func offerHasImage(mimes []string) bool {
	for _, m := range mimes {
		if strings.EqualFold(m, "image/png") {
			return true
		}
	}
	return false
}
