/* SPDX-License-Identifier: GPL-3.0-or-later */

use crate::check_space;
use crate::common::*;
use crate::wayland::*;
use crate::wayland_util::*;
use arrayvec::ArrayVec;
use log::warn;
use std::collections::BTreeMap;
use std::os::fd::OwnedFd;

struct OutputInfo {
    name: Option<String>,
}

/** Core protocol tracking state for `windowtolayer`, to convert xdg-shell clients to layer-shell.
 *
 * A downstream xdg_toplevel corresponds to an upstream zwlr_layer_surface_v1; the downstream
 * xdg_surface has no upstream equivalent.
 */
pub struct WindowToLayer<'a> {
    objs: ObjectTracker,

    /** Map xdg_surface objects to the wl_surface they extend. Mappings removed
     * on xdg_surface or wl_surface destruction. */
    xdg_to_wl_surface_map: BTreeMap<DownstreamID, DownstreamID>,
    wl_to_xdg_surface_map: BTreeMap<DownstreamID, DownstreamID>,

    /** Map xdg_surface objects to the xdg_toplevel created from them. Mappings
     * removed on xdg_surface or xdg_toplevel destruction; note that configures
     * to a zombie zwlr_layer_surface_v1 are dropped. */
    surface_to_toplevel_map: BTreeMap<DownstreamID, DownstreamID>,
    toplevel_to_surface_map: BTreeMap<DownstreamID, DownstreamID>,

    /** Used to delay sending of wl_display::delete_id events for downstream
     * only objects until the same time that the upstream compositor would have
     * done them. The documentation of wl_display::sync suggests that the
     * callbacks will be completed in order. */
    delete_id_callback_map: BTreeMap<UpstreamID, DownstreamID>,

    /** Set of outputs provided by the compositor, plus metadata. Only tracked
     * if [.target_output] is Some. */
    outputs: BTreeMap<UpstreamID, OutputInfo>,
    /** The first wl_registry object that is created; will be reset to None once
     * the compositor deletes the object id for this registry */
    first_registry: Option<UpstreamID>,
    // empty region that must be created to turn pointer interactivity off
    empty_region: Option<UpstreamID>,
    /** The first zwlr_layer_shell_v1 object created */
    zwlr_layer_shell_v1: Option<UpstreamID>,
    layer: ZwlrLayerShellV1Layer,
    zone: i32,
    maximized: bool,
    kbd_interactive: bool,
    mouse_interactive: bool,
    /** If Some, the exact wl_output::name of the output to choose when making a layer-shell surface */
    target_output: Option<&'a str>,
}

impl<'a> WindowToLayer<'a> {
    pub fn new(
        layer: ZwlrLayerShellV1Layer,
        zone: i32,
        kbd_int: bool,
        mouse_int: bool,
        maximized: bool,
        target_output: Option<&'a str>,
    ) -> Self {
        WindowToLayer {
            objs: ObjectTracker::default(),
            xdg_to_wl_surface_map: BTreeMap::new(),
            wl_to_xdg_surface_map: BTreeMap::new(),
            surface_to_toplevel_map: BTreeMap::new(),
            toplevel_to_surface_map: BTreeMap::new(),
            delete_id_callback_map: BTreeMap::new(),
            outputs: BTreeMap::new(),
            first_registry: None,
            zwlr_layer_shell_v1: None,
            layer,
            zone,
            kbd_interactive: kbd_int,
            mouse_interactive: mouse_int,
            maximized,
            empty_region: None,
            target_output,
        }
    }
}

impl MessageRewriter for WindowToLayer<'_> {
    fn process_message(
        &mut self,
        from_upstream: bool,
        msg: &[u8],
        fds: &mut ArrayVec<OwnedFd, FD_IN_QUEUE_SIZE>,
        dst: &mut OutputQueue,
        reverse_dst: &mut OutputQueue,
    ) -> Result<ProcResult, WaylandError> {
        if from_upstream {
            process_event_w2l(self, msg, fds, dst, reverse_dst)
        } else {
            process_request_w2l(self, msg, fds, dst, reverse_dst)
        }
    }
    fn log_message(&self, msg: &[u8], from_upstream: bool, processed: bool) {
        log_message(from_upstream, &self.objs, msg, processed);
    }
}

fn process_event_w2l(
    state: &mut WindowToLayer,
    msg: &[u8],
    fds: &mut ArrayVec<OwnedFd, FD_IN_QUEUE_SIZE>,
    dst: &mut OutputQueue,
    reverse_dst: &mut OutputQueue,
) -> Result<ProcResult, WaylandError> {
    use ProcResult::*;
    let object_id = UpstreamID(u32::from_le_bytes(msg[..4].try_into().unwrap()));
    let header = u32::from_le_bytes(msg[4..8].try_into().unwrap());
    let opcode = header & 0xffff;

    let Some(UpstreamObject {
        alt: alt_id,
        intf,
        version,
        zombie,
    }) = state.objs.up_to_down.get(&object_id).copied()
    else {
        return Err(WaylandError::Other(format!(
            "Event received for unidentified object {}",
            object_id
        )));
    };

    let Some(meth) = &intf.evts.get(opcode as usize) else {
        return Err(WaylandError::Other(format!(
            "Unidentified opcode {} for event to {}#{}",
            opcode, intf.name, object_id
        )));
    };

    if intf.uid == WL_REGISTRY.uid && opcode == (WlRegistryEvtIDs::Global as u32) {
        let (name, string, version) = parse_wl_registry_evt_global(msg)?;
        let opt_glob_intf = lookup_global_intf_by_name(string);

        if string.eq(XDG_WM_BASE.name.as_bytes()) {
            // do nothing, thereby filtering out the global
            Ok(Done)
        } else if string.eq(ZWLR_LAYER_SHELL_V1.name.as_bytes()) {
            /* Translate the upstream layer-shell advertisement to xdg_wm_base, to ensure
             * that xdg_wm_base is only advertised when the global required to translate it
             * is present. */
            let new_name = XDG_WM_BASE.name.as_bytes();
            let length = length_wl_registry_evt_global(new_name);

            let bind_layer_shell = state.zwlr_layer_shell_v1.is_none();
            let rev_length = if bind_layer_shell {
                length_wl_registry_req_bind(ZWLR_LAYER_SHELL_V1.name.as_bytes())
            } else {
                0
            };

            check_space!((length, 0), (rev_length, 0), dst, reverse_dst);

            write_wl_registry_evt_global(dst, alt_id.unwrap(), name, new_name, XDG_WM_BASE.version);

            if bind_layer_shell {
                /* Bind the first zwlr_layer_shell_v1 global advertised, to ensure it is available to create
                 * layer surfaces. */
                let layer_shell_id = get_new_upstream_client_id(&mut state.objs)?;
                insert_object(
                    &mut state.objs,
                    Some((layer_shell_id, &ZWLR_LAYER_SHELL_V1, version)),
                    None,
                )?;
                // TODO: this requires that the registry is not a zombie object /
                // was already not destroyed via wl_fixes
                write_wl_registry_req_bind(
                    reverse_dst,
                    object_id,
                    name,
                    string,
                    version,
                    layer_shell_id,
                );

                state.zwlr_layer_shell_v1 = Some(layer_shell_id);
            }

            Ok(Done)
        } else if let Some(glob_intf) = opt_glob_intf {
            let length = length_wl_registry_evt_global(string);
            let bind_output = if string.eq(WL_OUTPUT.name.as_bytes())
                && state.target_output.is_some()
                && state.first_registry == Some(object_id)
            {
                if version < 4 {
                    warn!(
                        "wl_output global with name={} was advertised with a version {} < 4 and \
                        its name cannot be detected, so picking an output with a \
                        matching name will probably fail",
                        name, version
                    );
                }
                version >= 4
            } else {
                false
            };

            let rev_length = if bind_output {
                length_wl_registry_req_bind(WL_OUTPUT.name.as_bytes())
            } else {
                0
            };
            check_space!((length, 0), (rev_length, 0), dst, reverse_dst);

            write_wl_registry_evt_global(
                dst,
                alt_id.unwrap(),
                name,
                string,
                std::cmp::min(version, glob_intf.version),
            );
            if bind_output {
                /* As the client is not guaranteed to bind any output, and getting output information
                 * requires a roundtrip, immediately bind all outputs ourselves as soon as possible
                 * to ensure wl_output::name and wl_output::done can possibly arrive, and the correct
                 * output to attach the layer surface to, can be identified, before an xdg_toplevel
                 * is made */
                // TODO: if the Wayland client later binds the output, redirect it to this
                // object, and later filter out object deletion; this will reduce the number
                // of repeated events the compositor must send for property updates and surface enter/leave
                // (However, doing this would make the proxy amplify the number of messages, which
                // could easily overflow the destination buffer, under the current MessageRewriter framework)
                let output_id = get_new_upstream_client_id(&mut state.objs)?;
                let output_version = 4;
                insert_object(
                    &mut state.objs,
                    Some((output_id, &WL_OUTPUT, output_version)),
                    None,
                )?;
                // TODO: this requires that the registry has not been deleted
                write_wl_registry_req_bind(
                    reverse_dst,
                    object_id,
                    name,
                    string,
                    output_version,
                    output_id,
                );

                state.outputs.insert(output_id, OutputInfo { name: None });
            }
            Ok(Done)
        } else {
            /* Drop global, unrecognized */
            Ok(Done)
        }
    } else if intf.uid == WL_OUTPUT.uid && alt_id.is_none() {
        /* Events directed solely toward wl_outputs created by this program */
        if let WlOutputEvtIDs::Name = parse_wl_output_evt_ids(opcode).unwrap() {
            let name = parse_wl_output_evt_name(msg)?;
            /* All Wayland message strings should be valid UTF-8. */
            let mut tmp: Option<String> = Some(
                std::str::from_utf8(name)
                    .map_err(|_| ParseError(()))?
                    .into(),
            );
            std::mem::swap(
                &mut state.outputs.get_mut(&object_id).unwrap().name,
                &mut tmp,
            );
        }
        Ok(Done)
    } else if intf.uid == WL_SURFACE.uid {
        /* For each created output, wl_surface::enter/::leave events are sent
         * corresponding to it; drop them */
        let drop = match parse_wl_surface_evt_ids(opcode).unwrap() {
            WlSurfaceEvtIDs::Enter => {
                let output = parse_wl_surface_evt_enter(msg)?;
                let Some(ds_output) = state.objs.up_to_down.get(&output).copied() else {
                    return Err(WaylandError::Other(
                        "wl_surface::enter wl_output object not recognized".to_string(),
                    ));
                };
                ds_output.alt.is_none()
            }
            WlSurfaceEvtIDs::Leave => {
                let output = parse_wl_surface_evt_leave(msg)?;
                let Some(ds_output) = state.objs.up_to_down.get(&output).copied() else {
                    return Err(WaylandError::Other(
                        "wl_surface::leave wl_output object not recognized".to_string(),
                    ));
                };
                ds_output.alt.is_none()
            }
            _ => false,
        };

        if !drop {
            check_space!((msg.len(), 0), (0, 0), dst, reverse_dst);
            create_objects(msg, version, meth, true, &mut state.objs)?;
            write_translate(msg, meth, true, &state.objs, fds, dst)?;
        }
        Ok(Done)
    } else if intf.uid == WP_PRESENTATION_FEEDBACK.uid
        && opcode == (WpPresentationFeedbackEvtIDs::SyncOutput as u32)
    {
        /* This is received for each wl_output corresponding to the underlying
         * output; drop messages for upstream only wl_outputs. */
        let output = parse_wp_presentation_feedback_evt_sync_output(msg)?;
        let Some(ds_output) = state.objs.up_to_down.get(&output).copied() else {
            return Err(WaylandError::Other(
                "wl_surface::enter wl_output object not recognized".to_string(),
            ));
        };
        let drop = ds_output.alt.is_none();

        if !drop {
            check_space!((msg.len(), 0), (0, 0), dst, reverse_dst);
            create_objects(msg, version, meth, true, &mut state.objs)?;
            write_translate(msg, meth, true, &state.objs, fds, dst)?;
        }
        Ok(Done)
    } else if intf.uid == ZWLR_LAYER_SHELL_V1.uid {
        /* Do nothing: layer shell has no events */
        Ok(Done)
    } else if intf.uid == ZWLR_LAYER_SURFACE_V1.uid {
        match parse_zwlr_layer_surface_v1_evt_ids(opcode).unwrap() {
            ZwlrLayerSurfaceV1EvtIDs::Configure => {
                if zombie {
                    /* Configures to a zombie object should be safe to drop
                     * as they don't create any new objects. */
                    return Ok(Done);
                }

                let toplevel_id = alt_id.unwrap();
                let Some(surface_id) = state.toplevel_to_surface_map.get(&toplevel_id).copied()
                else {
                    return Err(WaylandError::Other(
                        "zwlr_layer_surface_v1::configure applied to object of unknown source"
                            .to_string(),
                    ));
                };
                let toplevel_version = state.objs.down_to_up.get(&toplevel_id).unwrap().version;

                let mut states = Vec::new();
                states.extend_from_slice(&u32::to_le_bytes(XdgToplevelState::Fullscreen as u32));
                if state.maximized {
                    states.extend_from_slice(&u32::to_le_bytes(XdgToplevelState::Maximized as u32));
                }
                let mut length =
                    length_xdg_surface_evt_configure() + length_xdg_toplevel_evt_configure(&states);
                // todo: after the first time, only send configure_bounds/wm_capabilities when there is a change
                if toplevel_version >= 4 {
                    length += length_xdg_toplevel_evt_configure_bounds();
                }
                let capabilities = &[]; /* no window menu, maximize, fullscreen, or minimize controls available */
                if toplevel_version >= 5 {
                    length += length_xdg_toplevel_evt_wm_capabilities(capabilities);
                }

                check_space!((length, 0), (0, 0), dst, reverse_dst);

                let (serial, uwidth, uheight) = parse_zwlr_layer_surface_v1_evt_configure(msg)?;
                let Some((width, height)) = u32_to_i32_size(uwidth, uheight) else {
                    /* Alternatively, could saturate when converting; surfaces exceeding i32::MAX
                     * in any dimension are impractical anyway. */
                    return Err(WaylandError::Other(format!(
                        "Layer surface configure bounds width={} height={} are too large to fit in i32",
                        uwidth, uheight
                    )));
                };

                if toplevel_version >= 5 {
                    write_xdg_toplevel_evt_wm_capabilities(dst, toplevel_id, capabilities);
                }
                if toplevel_version >= 4 {
                    let (bound_w, bound_h) = if state.maximized {
                        /* Note: when maximized, client needs to exactly use configured size,
                         * so this technically isn't required. */
                        (width, height)
                    } else {
                        /* No constraint */
                        (0, 0)
                    };
                    write_xdg_toplevel_evt_configure_bounds(dst, toplevel_id, bound_w, bound_h);
                }
                write_xdg_toplevel_evt_configure(dst, toplevel_id, width, height, &states);
                write_xdg_surface_evt_configure(dst, surface_id, serial);
                Ok(Done)
            }
            ZwlrLayerSurfaceV1EvtIDs::Closed => {
                /* zwlr_layer_surface_v1::closed (notification that window has been closed)
                 * is stronger than xdg_toplevel::close (polite request to close).
                 * xdg_toplevel currently provides no event to unconditionally close a
                 * single window. (Dropping the client connection removes all windows.)
                 * Sending xdg_toplevel::close risks that certain applications may
                 * try to open a close warning popup/dialog.
                 */
                let length = length_xdg_toplevel_evt_close();
                check_space!((length, 0), (0, 0), dst, reverse_dst);
                write_xdg_toplevel_evt_close(dst, alt_id.unwrap());
                Ok(Done)
            }
        }
    } else if intf.uid == WL_CALLBACK.uid {
        assert!(matches!(
            parse_wl_callback_evt_ids(opcode).unwrap(),
            WlCallbackEvtIDs::Done
        ));

        if let Some(deleted_id) = state.delete_id_callback_map.get(&object_id).copied() {
            /* Handle delayed delete_id events */
            let length = length_wl_display_evt_delete_id();
            check_space!((length, 0), (0, 0), dst, reverse_dst);
            write_wl_display_evt_delete_id(dst, DownstreamID(1), deleted_id.0);

            state.delete_id_callback_map.remove(&object_id);
            let Some(obj) = state.objs.down_to_up.remove(&deleted_id) else {
                return Err(WaylandError::Other(format!(
                    "Received callback to delete id {}, which was already deleted",
                    deleted_id
                )));
            };
            if obj.alt.is_some() {
                return Err(WaylandError::Other(format!(
                    "Received callback to delete id {}, which was not downstream only",
                    deleted_id
                )));
            }
            Ok(Done)
        } else {
            check_space!((msg.len(), 0), (0, 0), dst, reverse_dst);
            create_objects(msg, version, meth, true, &mut state.objs)?;
            write_translate(msg, meth, true, &state.objs, fds, dst)?;
            generic_destroy_object(&mut state.objs, object_id, alt_id.unwrap())?;
            Ok(Done)
        }
    } else if intf.uid == WL_DISPLAY.uid && opcode == (WlDisplayEvtIDs::DeleteId as u32) {
        let length = length_wl_display_evt_delete_id();
        check_space!((length, 0), (0, 0), dst, reverse_dst);

        let del_id = UpstreamID(parse_wl_display_evt_delete_id(msg)?);
        if !del_id.is_client_id() {
            return Err(WaylandError::Other(format!(
                "Received wl_display::delete_id for non-client created ID {}",
                del_id
            )));
        }

        delete_upstream_client_id(&mut state.objs, del_id);

        let Some(obj) = state.objs.up_to_down.remove(&del_id) else {
            return Err(WaylandError::Other(format!(
                "Received wl_display::delete_id with unknown object id {}",
                del_id
            )));
        };
        let Some(ds_id) = obj.alt else {
            /* No matching downstream object, no need to propagate */
            return Ok(Done);
        };

        state
            .objs
            .down_to_up
            .remove(&ds_id)
            .expect("Should have matching entry in down_to_up");

        if let Some(u) = state.first_registry {
            if u == del_id {
                state.first_registry = None;
            }
        }
        write_wl_display_evt_delete_id(dst, alt_id.unwrap(), ds_id.0);
        Ok(Done)
    } else {
        check_space!((msg.len(), meth.fd_count.into()), (0, 0), dst, reverse_dst);
        create_objects(msg, version, meth, true, &mut state.objs)?;
        write_translate(msg, meth, true, &state.objs, fds, dst)?;
        if meth.destructor {
            generic_destroy_object(&mut state.objs, object_id, alt_id.unwrap())?;
        }
        Ok(Done)
    }
}
fn process_request_w2l(
    state: &mut WindowToLayer,
    msg: &[u8],
    fds: &mut ArrayVec<OwnedFd, FD_IN_QUEUE_SIZE>,
    dst: &mut OutputQueue,
    reverse_dst: &mut OutputQueue,
) -> Result<ProcResult, WaylandError> {
    use ProcResult::*;
    let object_id = DownstreamID(u32::from_le_bytes(msg[..4].try_into().unwrap()));
    let header = u32::from_le_bytes(msg[4..8].try_into().unwrap());
    let opcode = header & 0xffff;

    /* Process message */
    let Some(DownstreamObject {
        alt: alt_id,
        intf,
        version,
    }) = state.objs.down_to_up.get(&object_id).copied()
    else {
        return Err(WaylandError::Other(format!(
            "Request received for unidentified object {}",
            object_id
        )));
    };
    let Some(meth) = &intf.reqs.get(opcode as usize) else {
        return Err(WaylandError::Other(format!(
            "Unidentified opcode {} for request to {}#{}",
            opcode, intf.name, object_id
        )));
    };

    if intf.uid == WL_DISPLAY.uid && opcode == (WlDisplayReqIDs::GetRegistry as u32) {
        check_space!((msg.len(), 0), (0, 0), dst, reverse_dst);
        let downstream_id = parse_wl_display_req_get_registry(msg)?;
        let upstream_id = get_new_upstream_client_id(&mut state.objs)?;

        insert_object(
            &mut state.objs,
            Some((upstream_id, &WL_REGISTRY, 1)),
            Some((downstream_id, &WL_REGISTRY, 1)),
        )?;
        if state.first_registry.is_none() {
            // Note: odd behavior is possible if the registry is destroyed later. It may be entirely
            // avoidable if a registry is kept alive indefinitely, either filtering wl_fixes requests
            // to never destroy the registry, or making a new registry object (which is wasteful).
            state.first_registry = Some(upstream_id);
        }
        write_wl_display_req_get_registry(dst, alt_id.unwrap(), upstream_id);
        Ok(Done)
    } else if intf.uid == WL_REGISTRY.uid && opcode == (WlRegistryReqIDs::Bind as u32) {
        let (name, string, bind_version, id) = parse_wl_registry_req_bind(msg)?;

        let bind_intf = lookup_global_intf_by_name(string).ok_or_else(|| {
            WaylandError::Other(format!(
                "Unidentified interface to bind: \"{}\"",
                string.escape_ascii()
            ))
        })?;
        if bind_intf.uid == XDG_WM_BASE.uid {
            if state.zwlr_layer_shell_v1.is_none() {
                return Err(WaylandError::Other(
                    "Client tried to bind xdg_wm_base before it was advertised.".to_string(),
                ));
            }
            if bind_version == 0 || bind_version > XDG_WM_BASE.version {
                return Err(WaylandError::Other(
                    "Invalid wl_registry::bind version for xdg_wm_base".to_string(),
                ));
            }
            /* Create this object downstream only */
            insert_object(
                &mut state.objs,
                None,
                Some((id, &XDG_WM_BASE, bind_version)),
            )?;

            Ok(Done)
        } else {
            let make_empty_region = !state.mouse_interactive
                && string == WL_COMPOSITOR.name.as_bytes()
                && state.empty_region.is_none();

            let length = length_wl_registry_req_bind(string)
                + (if make_empty_region {
                    length_wl_compositor_req_create_region()
                } else {
                    0
                });
            check_space!((length, 0), (0, 0), dst, reverse_dst);

            let upstream_id = get_new_upstream_client_id(&mut state.objs)?;
            write_wl_registry_req_bind(
                dst,
                alt_id.unwrap(),
                name,
                string,
                bind_version,
                upstream_id,
            );
            if make_empty_region {
                let reg_id = get_new_upstream_client_id(&mut state.objs)?;
                state.empty_region = Some(reg_id);
                write_wl_compositor_req_create_region(dst, upstream_id, reg_id);
                insert_object(
                    &mut state.objs,
                    Some((reg_id, &WL_REGION, bind_version)),
                    None,
                )?;
            }

            insert_object(
                &mut state.objs,
                Some((upstream_id, bind_intf, bind_version)),
                Some((id, bind_intf, bind_version)),
            )?;
            Ok(Done)
        }
    } else if intf.uid == WL_COMPOSITOR.uid
        && (opcode == WlCompositorReqIDs::CreateSurface as u32)
        && !state.mouse_interactive
    {
        let length =
            length_wl_compositor_req_create_surface() + length_wl_surface_req_set_input_region();
        check_space!((length, 0), (0, 0), dst, reverse_dst);

        assert!(state.empty_region.is_some());
        let surf_id = parse_wl_compositor_req_create_surface(msg)?;
        let alt_surf_id = get_new_upstream_client_id(&mut state.objs)?;

        insert_object(
            &mut state.objs,
            Some((alt_surf_id, &WL_SURFACE, version)),
            Some((surf_id, &WL_SURFACE, version)),
        )?;

        write_wl_compositor_req_create_surface(dst, alt_id.unwrap(), alt_surf_id);
        write_wl_surface_req_set_input_region(dst, alt_surf_id, state.empty_region.unwrap());
        Ok(Done)
    } else if intf.uid == WL_SURFACE.uid
        && (opcode == WlSurfaceReqIDs::SetInputRegion as u32)
        && !state.mouse_interactive
    {
        // Drop all requests to control the input region, as in this case
        // it is being injected on surface creation
        Ok(Done)
    } else if intf.uid == WL_SURFACE.uid && (opcode == WlSurfaceReqIDs::Destroy as u32) {
        check_space!((msg.len(), 0), (0, 0), dst, reverse_dst);
        write_translate(msg, meth, false, &state.objs, fds, dst)?;
        generic_destroy_object(&mut state.objs, alt_id.unwrap(), object_id)?;
        /* Cleanup wl_surface to xdg_toplevel link, if it existed */
        if let Some(xdg_surface_id) = state.wl_to_xdg_surface_map.remove(&object_id) {
            assert!(state
                .xdg_to_wl_surface_map
                .remove(&xdg_surface_id)
                .is_some());
        }
        Ok(Done)
    } else if intf.uid == ZXDG_DECORATION_MANAGER_V1.uid {
        match parse_zxdg_decoration_manager_v1_req_ids(opcode).unwrap() {
            ZxdgDecorationManagerV1ReqIDs::Destroy => { /* todo */ }
            ZxdgDecorationManagerV1ReqIDs::GetToplevelDecoration => {
                /* create a downstream only toplevel object, and link it to the xdg-surface */
                let (new_decoration_id, _xdg_toplevel_id) =
                    parse_zxdg_decoration_manager_v1_req_get_toplevel_decoration(msg)?;

                // TODO: associate the decoration with the surface

                /* this object has _no_ corresponding object on the other side */
                insert_object(
                    &mut state.objs,
                    None,
                    Some((new_decoration_id, &ZXDG_TOPLEVEL_DECORATION_V1, version)),
                )?;
            }
        }
        Ok(Done)
    } else if intf.uid == ZXDG_TOPLEVEL_DECORATION_V1.uid {
        /* Ignore all */
        Ok(Done)
    } else if intf.uid == XDG_WM_BASE.uid {
        match parse_xdg_wm_base_req_ids(opcode).unwrap() {
            XdgWmBaseReqIDs::GetXdgSurface => {
                let (new_xdg_surface_id, wl_surface_id) =
                    parse_xdg_wm_base_req_get_xdg_surface(msg)?;
                let Some(wl_surface_obj) = state.objs.down_to_up.get(&wl_surface_id).copied()
                else {
                    return Err(WaylandError::Other(
                        "xdg_wm_base::get_xdg_surface has invalid wl_surface id".to_string(),
                    ));
                };
                if wl_surface_obj.intf.uid != WL_SURFACE.uid {
                    return Err(WaylandError::Other(
                        "xdg_wm_base::get_xdg_surface has object that is not a wl_surface"
                            .to_string(),
                    ));
                }

                /* this object has _no_ corresponding object on the other side */
                insert_object(
                    &mut state.objs,
                    None,
                    Some((new_xdg_surface_id, &XDG_SURFACE, version)),
                )?;
                state
                    .xdg_to_wl_surface_map
                    .insert(new_xdg_surface_id, wl_surface_id);
                Ok(Done)
            }
            XdgWmBaseReqIDs::CreatePositioner => Err(WaylandError::Other(
                "unimplemented: xdg_wm_base::create_positioner".to_string(),
            )),
            XdgWmBaseReqIDs::Pong => Err(WaylandError::Other(
                "Did not expect to receive xdg_wm_base::pong; ping was never sent".to_string(),
            )),
            XdgWmBaseReqIDs::Destroy => {
                check_space!((length_wl_display_req_sync(), 0), (0, 0), dst, reverse_dst);

                let callback_id = get_new_upstream_client_id(&mut state.objs)?;
                insert_object(&mut state.objs, Some((callback_id, &WL_CALLBACK, 1)), None)?;
                state.delete_id_callback_map.insert(callback_id, object_id);
                write_wl_display_req_sync(dst, UpstreamID(1), callback_id);
                Ok(Done)
            }
        }
    } else if intf.uid == XDG_SURFACE.uid {
        match parse_xdg_surface_req_ids(opcode).unwrap() {
            XdgSurfaceReqIDs::Destroy => {
                check_space!((length_wl_display_req_sync(), 0), (0, 0), dst, reverse_dst);

                /* The protocol requires xdg_surface objects to be destroyed
                 * after their role object e.g. (xdg_toplevel) is destroyed */
                if let Some(toplevel_id) = state.surface_to_toplevel_map.get(&object_id) {
                    return Err(WaylandError::Other(format!("xdg_surface#(_,{}) should have been destroyed after corresponding xdg_toplevel#(...,{})", object_id, toplevel_id)));
                }
                if let Some(wl_surface_id) = state.xdg_to_wl_surface_map.remove(&object_id) {
                    state.wl_to_xdg_surface_map.remove(&wl_surface_id);
                }

                /* Implement wl_display::delete_id with a callback, to avoid it returning
                 * earlier than expected. This is probably unnecessary, but clients
                 * directly manipulating the protocol could use destruction + ::delete_id
                 * as a ::sync alternative. */
                let callback_id = get_new_upstream_client_id(&mut state.objs)?;
                insert_object(&mut state.objs, Some((callback_id, &WL_CALLBACK, 1)), None)?;
                state.delete_id_callback_map.insert(callback_id, object_id);
                write_wl_display_req_sync(dst, UpstreamID(1), callback_id);

                Ok(Done)
            }
            XdgSurfaceReqIDs::GetToplevel => {
                let target_output_id = if let Some(ref name) = state.target_output {
                    let mut id = UpstreamID(0);
                    let mut complete = true;
                    for (output, val) in state.outputs.iter() {
                        if let Some(ref vname) = val.name {
                            if vname == name {
                                id = *output;
                                break;
                            }
                        } else {
                            complete = false;
                        }
                    }
                    if id != UpstreamID(0) {
                        id
                    } else if complete {
                        let mut output_list: Vec<&str> = state
                            .outputs
                            .values()
                            .map(|x| x.name.as_ref().unwrap().as_str())
                            .collect();
                        output_list.sort();

                        return Err(WaylandError::Other(format!(
                            "Output \"{}\" was not found, exiting. There {} {} {}: {}",
                            name,
                            if output_list.len() == 1 { "is" } else { "are" },
                            output_list.len(),
                            if output_list.len() == 1 {
                                "output"
                            } else {
                                "outputs"
                            },
                            QuotedStrings(output_list)
                        )));
                    } else {
                        return Ok(WaitForOtherDirection);
                    }
                } else {
                    UpstreamID(0)
                };

                let namespace = &[];
                let length = length_zwlr_layer_shell_v1_req_get_layer_surface(namespace)
                    + length_zwlr_layer_surface_v1_req_set_anchor()
                    + length_zwlr_layer_surface_v1_req_set_exclusive_zone()
                    + (if state.kbd_interactive {
                        length_zwlr_layer_surface_v1_req_set_keyboard_interactivity()
                    } else {
                        0
                    });
                check_space!((length, 0), (0, 0), dst, reverse_dst);

                let new_toplevel_id = parse_xdg_surface_req_get_toplevel(msg)?;
                let Some(wl_surface_id) = state.xdg_to_wl_surface_map.get(&object_id) else {
                    return Err(WaylandError::Other(
                        "Unexpected xdg_surface: created without associated wl_surface".to_string(),
                    ));
                };
                let upstream_wl_surface_id = state.objs.down_to_up[wl_surface_id].alt.unwrap();

                let layer_surface_id = get_new_upstream_client_id(&mut state.objs)?;

                let layer_shell = state.zwlr_layer_shell_v1.unwrap();
                let layer_shell_version = state.objs.up_to_down.get(&layer_shell).unwrap().version;
                write_zwlr_layer_shell_v1_req_get_layer_surface(
                    dst,
                    layer_shell,
                    layer_surface_id,
                    upstream_wl_surface_id,
                    target_output_id,
                    state.layer as u32,
                    namespace,
                );
                let anchor = ZwlrLayerSurfaceV1Anchor::Left as u32
                    | ZwlrLayerSurfaceV1Anchor::Top as u32
                    | ZwlrLayerSurfaceV1Anchor::Right as u32
                    | ZwlrLayerSurfaceV1Anchor::Bottom as u32;
                write_zwlr_layer_surface_v1_req_set_anchor(dst, layer_surface_id, anchor);
                write_zwlr_layer_surface_v1_req_set_exclusive_zone(
                    dst,
                    layer_surface_id,
                    state.zone,
                );
                if state.kbd_interactive {
                    write_zwlr_layer_surface_v1_req_set_keyboard_interactivity(
                        dst,
                        layer_surface_id,
                        ZwlrLayerSurfaceV1KeyboardInteractivity::OnDemand as u32,
                    );
                }

                state
                    .surface_to_toplevel_map
                    .insert(object_id, new_toplevel_id);
                state
                    .toplevel_to_surface_map
                    .insert(new_toplevel_id, object_id);

                insert_object(
                    &mut state.objs,
                    Some((
                        layer_surface_id,
                        &ZWLR_LAYER_SURFACE_V1,
                        layer_shell_version,
                    )),
                    Some((new_toplevel_id, &XDG_TOPLEVEL, version)),
                )?;
                Ok(Done)
            }
            XdgSurfaceReqIDs::GetPopup => {
                // technically, these can be forwarded
                Err(WaylandError::Other(
                    "unimplemented: xdg_surface::get_popup".to_string(),
                ))
            }
            XdgSurfaceReqIDs::SetWindowGeometry => {
                /* ignore, for now */
                Ok(Done)
            }
            XdgSurfaceReqIDs::AckConfigure => {
                let length = length_zwlr_layer_surface_v1_req_ack_configure();
                check_space!((length, 0), (0, 0), dst, reverse_dst);
                let serial = parse_xdg_surface_req_ack_configure(msg)?;
                let Some(toplevel) = state.surface_to_toplevel_map.get(&object_id) else {
                    /* xdg_surface::ack_configure received, but there is no associated toplevel.
                     * This could be a tracking error here or at the compositor (if the surface
                     * role is not toplevel), but it is also possible that the toplevel was
                     * destroyed by the client. */
                    return Ok(Done);
                };
                let layer_surface = state.objs.down_to_up.get(toplevel).unwrap().alt.unwrap();
                write_zwlr_layer_surface_v1_req_ack_configure(dst, layer_surface, serial);
                Ok(Done)
            }
        }
    } else if intf.uid == XDG_TOPLEVEL.uid {
        match parse_xdg_toplevel_req_ids(opcode).unwrap() {
            XdgToplevelReqIDs::SetMaxSize => {
                if state.maximized {
                    /* Accept whatever the compositor sends in response to the default
                     * layer shell surface size (which is 0,0 = compositor preference) */
                    return Ok(Done);
                }

                /* Translate set_max_size requests to layer shell;
                 * set_min_size requests have not equivalent and will be ignored */
                let length = length_zwlr_layer_surface_v1_req_set_size()
                    + length_zwlr_layer_surface_v1_req_set_anchor();
                check_space!((length, 0), (0, 0), dst, reverse_dst);

                /* xdg_toplevel::set_max_size and zwlr_layer_surface_v1::set_size have the
                 * same 0=unspecified handling; note ::set_size requires the unbound dimensions
                 * to be anchored to both sides */
                let (width, height) = parse_xdg_toplevel_req_set_max_size(msg)?;
                let layer_surface_id = state.objs.down_to_up.get(&object_id).unwrap().alt.unwrap();
                let Some((uwidth, uheight)) = i32_to_u32_size(width, height) else {
                    return Err(WaylandError::Other(format!(
                        "width={} and height={} arguments of \
                        xdg_toplevel_surface::set_max_size should be nonnegative",
                        width, height
                    )));
                };

                write_zwlr_layer_surface_v1_req_set_size(dst, layer_surface_id, uwidth, uheight);
                /* Compositors differ in how they choose sizes in response to anchor=0 vs anchor=all
                 * for wlr-layer-shell. Prefer centering by anchoring to nothing instead of to parallel
                 * axes. However, ::set_size requires parallel anchors when a 0 value is given. */
                let mut anchor = 0;
                if width == 0 {
                    anchor |= (ZwlrLayerSurfaceV1Anchor::Left as u32)
                        | (ZwlrLayerSurfaceV1Anchor::Right as u32);
                }
                if height == 0 {
                    anchor |= (ZwlrLayerSurfaceV1Anchor::Top as u32)
                        | (ZwlrLayerSurfaceV1Anchor::Bottom as u32);
                }
                write_zwlr_layer_surface_v1_req_set_anchor(dst, layer_surface_id, anchor);
                Ok(Done)
            }
            XdgToplevelReqIDs::Destroy => {
                /* Destroying the toplevel unmaps the surface, so this should trigger
                 * zwlr_layer_surface_v1::destroy. Note: wl_surface permits roles to be
                 * recreated if not forbidden, which the xdg-shell protocol does not appear
                 * to do; thus after destroying a toplevel a weird client may be able
                 * to add a new xdg_toplevel role object, yielding a new zwlr_layer_surface_v1.
                 * (But not any other role type.) */
                let length = length_zwlr_layer_shell_v1_req_destroy();
                check_space!((length, 0), (0, 0), dst, reverse_dst);
                write_zwlr_layer_surface_v1_req_destroy(dst, alt_id.unwrap());
                generic_destroy_object(&mut state.objs, alt_id.unwrap(), object_id)?;
                if let Some(xdg_surface_id) = state.toplevel_to_surface_map.remove(&object_id) {
                    state.surface_to_toplevel_map.remove(&xdg_surface_id);
                }
                Ok(Done)
            }
            _ => {
                /* Other messages have no direct layer-shell equivalent, although set_min_size/set_max_size
                 * may be adaptable */
                Ok(Done)
            }
        }
    } else if intf.uid == WL_FIXES.uid && opcode == (WlFixesReqIDs::DestroyRegistry as u32) {
        check_space!((msg.len(), meth.fd_count.into()), (0, 0), dst, reverse_dst);
        let reg = parse_wl_fixes_req_destroy_registry(msg)?;
        let Some(upstream_reg) = state.objs.down_to_up.get(&reg).copied() else {
            return Err(WaylandError::Other(
                "Tried to destroy non-existent object with wl_fixes::destroy_registry".to_string(),
            ));
        };
        if upstream_reg.intf.uid != WL_REGISTRY.uid {
            return Err(WaylandError::Other(
                "Tried to destroy non-registry object with wl_fixes::destroy_registry".to_string(),
            ));
        }
        write_wl_fixes_req_destroy_registry(dst, alt_id.unwrap(), upstream_reg.alt.unwrap());
        generic_destroy_object(&mut state.objs, upstream_reg.alt.unwrap(), reg)?;
        Ok(Done)
    } else {
        check_space!((msg.len(), meth.fd_count.into()), (0, 0), dst, reverse_dst);
        create_objects(msg, version, meth, false, &mut state.objs)?;
        write_translate(msg, meth, false, &state.objs, fds, dst)?;
        if meth.destructor {
            generic_destroy_object(&mut state.objs, alt_id.unwrap(), object_id)?;
        }
        Ok(Done)
    }
}
