mod screencast_gui;

use {
    crate::{
        allocator::{AllocatorError, BO_USE_RENDERING, BufferObject, BufferUsage},
        dbus::{DbusObject, DictEntry, PendingReply, prelude::Variant},
        format::{Format, XRGB8888},
        ifs::{jay_compositor::GET_TOPLEVEL_SINCE, jay_screencast::CLIENT_BUFFERS_SINCE},
        pipewire::{
            pw_con::PwCon,
            pw_ifs::pw_client_node::{
                PwClientNode, PwClientNodeBufferConfig, PwClientNodeOwner, PwClientNodePort,
                PwClientNodePortSupportedFormat, PwClientNodePortSupportedFormats,
                SUPPORTED_META_VIDEO_CROP,
            },
            pw_pod::{
                PwPodRectangle, SPA_DATA_DmaBuf, SPA_MEDIA_SUBTYPE_raw, SPA_MEDIA_TYPE_video,
                SPA_STATUS_HAVE_DATA, SPA_VIDEO_FORMAT_UNKNOWN, SpaChunkFlags, spa_point,
                spa_rectangle, spa_region,
            },
        },
        portal::{
            PORTAL_SUCCESS, PortalState,
            ptl_display::{PortalDisplay, PortalDisplayId, PortalOutput},
            ptl_remote_desktop::RemoteDesktopPhase,
            ptl_screencast::screencast_gui::SelectionGui,
            ptl_session::{PortalSession, PortalSessionReply},
        },
        utils::{
            clonecell::{CloneCell, UnsafeCellCloneSafe},
            copyhashmap::CopyHashMap,
            errorfmt::ErrorFmt,
            opaque::Opaque,
        },
        video::{LINEAR_MODIFIER, Modifier, dmabuf::DmaBuf},
        wire::jay_screencast::Ready,
        wire_dbus::{
            org,
            org::freedesktop::impl_::portal::{
                screen_cast::{
                    CreateSession, CreateSessionReply, SelectSources, SelectSourcesReply, Start,
                    StartReply,
                },
                session::CloseReply as SessionCloseReply,
            },
        },
        wl_usr::usr_ifs::{
            usr_jay_screencast::{
                UsrJayScreencast, UsrJayScreencastOwner, UsrJayScreencastServerConfig,
            },
            usr_jay_select_toplevel::UsrJaySelectToplevel,
            usr_jay_select_workspace::UsrJaySelectWorkspace,
            usr_jay_toplevel::UsrJayToplevel,
            usr_jay_workspace::UsrJayWorkspace,
            usr_linux_buffer_params::{UsrLinuxBufferParams, UsrLinuxBufferParamsOwner},
            usr_wl_buffer::UsrWlBuffer,
        },
    },
    serde::{Deserialize, Serialize},
    std::{
        borrow::Cow,
        cell::{Cell, RefCell},
        ops::Deref,
        rc::Rc,
        sync::atomic::Ordering::{Acquire, Relaxed, Release},
    },
    thiserror::Error,
};

#[derive(Clone)]
pub enum ScreencastPhase {
    Init,
    SourcesSelected(Rc<SourcesSelectedScreencast>),
    Selecting(Rc<SelectingScreencast>),
    SelectingWindow(Rc<SelectingWindowScreencast>),
    SelectingWorkspace(Rc<SelectingWorkspaceScreencast>),
    Starting(Rc<StartingScreencast>),
    Started(Rc<StartedScreencast>),
    Terminated,
}

unsafe impl UnsafeCellCloneSafe for ScreencastPhase {}

pub struct SourcesSelectedScreencast {
    pub restore_data: Cell<Option<Result<RestoreData, RestoreError>>>,
}

#[derive(Clone)]
pub struct SelectingScreencastCore {
    pub session: Rc<PortalSession>,
    pub request_obj: Rc<DbusObject>,
}

pub struct SelectingScreencast {
    pub core: SelectingScreencastCore,
    pub guis: CopyHashMap<PortalDisplayId, Rc<SelectionGui>>,
    pub restore_data: Cell<Option<RestoreData>>,
}

pub struct SelectingWindowScreencast {
    pub core: SelectingScreencastCore,
    pub dpy: Rc<PortalDisplay>,
    pub selector: Rc<UsrJaySelectToplevel>,
    pub restoring: bool,
}

pub struct SelectingWorkspaceScreencast {
    pub core: SelectingScreencastCore,
    pub dpy: Rc<PortalDisplay>,
    pub selector: Rc<UsrJaySelectWorkspace>,
}

pub struct StartingScreencast {
    pub session: Rc<PortalSession>,
    pub _request_obj: Rc<DbusObject>,
    pub node: Rc<PwClientNode>,
    pub dpy: Rc<PortalDisplay>,
    pub target: ScreencastTarget,
}

pub enum ScreencastTarget {
    Output(Rc<PortalOutput>),
    Workspace(Rc<PortalOutput>, Rc<UsrJayWorkspace>, bool),
    Toplevel(Rc<UsrJayToplevel>),
}

pub struct StartedScreencast {
    pub session: Rc<PortalSession>,
    pub node: Rc<PwClientNode>,
    pub port: Rc<PwClientNodePort>,
    pub buffer_objects: RefCell<Vec<Rc<dyn BufferObject>>>,
    pub buffers: RefCell<Vec<DmaBuf>>,
    pub pending_buffers: RefCell<Vec<Rc<UsrLinuxBufferParams>>>,
    pub buffers_valid: Cell<bool>,
    pub dpy: Rc<PortalDisplay>,
    pub jay_screencast: Rc<UsrJayScreencast>,
    pub port_buffer_valid: Cell<bool>,
    pub fixated: Cell<bool>,
    pub format: Cell<&'static Format>,
    pub modifier: Cell<Modifier>,
    pub width: Cell<i32>,
    pub height: Cell<i32>,
}

bitflags! {
    CursorModes: u32;

    HIDDEN = 1,
    EMBEDDED = 2,
    METADATA = 4,
}

bitflags! {
    SourceTypes: u32;

    MONITOR = 1,
    WINDOW = 2,
}

impl PwClientNodeOwner for StartingScreencast {
    fn bound_id(&self, node_id: u32) {
        {
            let output = match &self.target {
                ScreencastTarget::Output(o) => Some(o),
                ScreencastTarget::Workspace(o, _, _) => Some(o),
                ScreencastTarget::Toplevel(_) => None,
            };
            let mapping_id = output.and_then(|o| o.wl.name.borrow().clone());
            self.session.send_start_reply(
                Some(node_id),
                create_restore_data(&self.dpy, &self.target),
                mapping_id.as_deref(),
            );
        }
        let mut supported_formats = PwClientNodePortSupportedFormats {
            media_type: Some(SPA_MEDIA_TYPE_video),
            media_sub_type: Some(SPA_MEDIA_SUBTYPE_raw),
            video_size: None,
            formats: vec![PwClientNodePortSupportedFormat {
                format: XRGB8888,
                modifiers: vec![LINEAR_MODIFIER],
            }],
        };
        init_supported_formats(&mut supported_formats, &self.dpy);
        let jsc_version = self.dpy.jc.version;
        let num_buffers = (jsc_version >= CLIENT_BUFFERS_SINCE).then_some(3);
        let port = self.node.create_port(true, supported_formats, num_buffers);
        port.can_alloc_buffers.set(true);
        port.supported_metas.set(SUPPORTED_META_VIDEO_CROP);
        let jsc = self.dpy.jc.create_screencast();
        match &self.target {
            ScreencastTarget::Output(o) => {
                jsc.set_output(&o.jay);
                jsc.set_allow_all_workspaces(true);
            }
            ScreencastTarget::Workspace(o, ws, _) => {
                jsc.set_output(&o.jay);
                jsc.allow_workspace(ws);
            }
            ScreencastTarget::Toplevel(t) => jsc.set_toplevel(t),
        }
        jsc.set_use_linear_buffers(true);
        jsc.configure();
        match &self.target {
            ScreencastTarget::Output(_) => {}
            ScreencastTarget::Workspace(_, w, true) => {
                self.dpy.con.remove_obj(&**w);
            }
            ScreencastTarget::Workspace(_, _, false) => {}
            ScreencastTarget::Toplevel(t) => {
                self.dpy.con.remove_obj(&**t);
            }
        }
        let started = Rc::new(StartedScreencast {
            session: self.session.clone(),
            node: self.node.clone(),
            port,
            buffer_objects: Default::default(),
            buffers: Default::default(),
            pending_buffers: Default::default(),
            buffers_valid: Cell::new(false),
            dpy: self.dpy.clone(),
            jay_screencast: jsc,
            port_buffer_valid: Cell::new(false),
            fixated: Cell::new(jsc_version < CLIENT_BUFFERS_SINCE),
            format: Cell::new(XRGB8888),
            modifier: Cell::new(LINEAR_MODIFIER),
            width: Cell::new(1),
            height: Cell::new(1),
        });
        self.session
            .sc_phase
            .set(ScreencastPhase::Started(started.clone()));
        started.jay_screencast.owner.set(Some(started.clone()));
        self.node.owner.set(Some(started.clone()));
    }
}

impl PwClientNodeOwner for StartedScreencast {
    fn port_format_changed(&self, port: &Rc<PwClientNodePort>) {
        let format = &*port.negotiated_format.borrow();
        if self.fixated.get() {
            return;
        }
        let (Some(fmt), Some(modifiers)) = (format.format, &format.modifiers) else {
            return;
        };
        let modifier;
        let planes;
        match self.allocate_buffer(fmt, modifiers) {
            Ok(bo) => {
                let dmabuf = bo.dmabuf();
                modifier = dmabuf.modifier;
                planes = dmabuf.planes.len();
            }
            Err(e) => {
                log::error!("Could not allocate buffer: {}", ErrorFmt(e));
                self.session.kill();
                return;
            }
        };
        log::debug!(
            "Negotiated format {} with modifier 0x{modifier:08x} at size {}x{}",
            fmt.name,
            self.width.get(),
            self.height.get(),
        );
        self.port.supported_formats.borrow_mut().formats = vec![PwClientNodePortSupportedFormat {
            format: fmt,
            modifiers: vec![modifier],
        }];
        self.port.buffer_config.borrow_mut().planes = Some(planes);
        self.node.send_port_update(&self.port, true);
        self.format.set(fmt);
        self.modifier.set(modifier);
        self.fixated.set(true);
    }

    fn use_buffers(self: Rc<Self>, port: &Rc<PwClientNodePort>) {
        if self.jay_screencast.version < CLIENT_BUFFERS_SINCE {
            self.node
                .send_port_output_buffers(port, &self.buffers.borrow_mut());
            self.buffers_valid.set(true);
            return;
        }
        self.buffers_valid.set(false);
        self.port_buffer_valid.set(false);
        let Some(dmabuf) = self.dpy.dmabuf.get() else {
            log::error!("Display does not support dmabuf");
            self.session.kill();
            return;
        };
        self.jay_screencast.clear_buffers();
        self.jay_screencast.configure();
        self.buffer_objects.borrow_mut().clear();
        self.buffers.borrow_mut().clear();
        for buffer in self.pending_buffers.borrow_mut().drain(..) {
            self.dpy.con.remove_obj(&*buffer);
        }
        for _ in 0..self.port.buffers.borrow().len() {
            let res = self.allocate_buffer(self.format.get(), &[self.modifier.get()]);
            match res {
                Ok(b) => {
                    let params = dmabuf.create_params();
                    params.create(&b.dmabuf());
                    params.owner.set(Some(self.clone()));
                    self.buffers.borrow_mut().push(b.dmabuf().clone());
                    self.buffer_objects.borrow_mut().push(b);
                    self.pending_buffers.borrow_mut().push(params);
                }
                Err(e) => {
                    log::error!("Could not allocate buffer: {}", ErrorFmt(e));
                    self.session.kill();
                    return;
                }
            }
        }
        self.node
            .send_port_output_buffers(&self.port, &self.buffers.borrow());
    }

    fn start(self: Rc<Self>) {
        self.jay_screencast.set_running(true);
        self.jay_screencast.configure();
    }

    fn pause(self: Rc<Self>) {
        self.jay_screencast.set_running(false);
        self.jay_screencast.configure();
    }

    fn suspend(self: Rc<Self>) {
        self.jay_screencast.set_running(false);
        self.jay_screencast.configure();
    }
}

#[derive(Debug, Error)]
enum BufferAllocationError {
    #[error("Display has no render context")]
    NoRenderContext,
    #[error(transparent)]
    Allocator(#[from] AllocatorError),
}

impl StartedScreencast {
    fn allocate_buffer(
        &self,
        format: &'static Format,
        modifiers: &[Modifier],
    ) -> Result<Rc<dyn BufferObject>, BufferAllocationError> {
        let Some(ctx) = self.dpy.render_ctx.get() else {
            return Err(BufferAllocationError::NoRenderContext);
        };
        let mut usage = BO_USE_RENDERING;
        if let Some(sf) = &ctx.server_formats
            && let Some(format) = sf.get(&format.drm)
        {
            let no_render_usage = modifiers.iter().all(|m| {
                format
                    .write_modifiers
                    .get(m)
                    .map(|w| !w.needs_render_usage)
                    .unwrap_or(false)
            });
            if no_render_usage {
                usage = BufferUsage::none();
            }
        }
        let buffer = ctx.ctx.ctx.allocator().create_bo(
            &self.dpy.state.dma_buf_ids,
            self.width.get(),
            self.height.get(),
            format,
            modifiers,
            usage,
        )?;
        Ok(buffer)
    }
}

impl SelectingScreencastCore {
    pub fn starting(&self, dpy: &Rc<PortalDisplay>, target: ScreencastTarget) {
        let Some(pw_con) = &self.session.pw_con else {
            self.session.kill();
            return;
        };
        let node = pw_con.create_client_node(&[
            ("media.class".to_string(), "Video/Source".to_string()),
            ("node.name".to_string(), "jay-desktop-portal".to_string()),
            ("node.driver".to_string(), "true".to_string()),
        ]);
        let starting = Rc::new(StartingScreencast {
            session: self.session.clone(),
            _request_obj: self.request_obj.clone(),
            node,
            dpy: dpy.clone(),
            target,
        });
        self.session
            .sc_phase
            .set(ScreencastPhase::Starting(starting.clone()));
        starting.node.owner.set(Some(starting.clone()));
        dpy.sessions.set(
            self.session.session_obj.path().to_owned(),
            self.session.clone(),
        );
    }
}

impl PortalSession {
    fn dbus_select_sources(
        self: &Rc<Self>,
        req: SelectSources,
        reply: PendingReply<SelectSourcesReply<'static>>,
    ) {
        match self.sc_phase.get() {
            ScreencastPhase::Init => {}
            _ => {
                self.kill();
                reply.err("Sources have already been selected");
                return;
            }
        }
        self.sc_phase.set(ScreencastPhase::SourcesSelected(Rc::new(
            SourcesSelectedScreencast {
                restore_data: Cell::new(get_restore_data(&req)),
            },
        )));
        reply.ok(&SelectSourcesReply {
            response: PORTAL_SUCCESS,
            results: Default::default(),
        });
    }

    fn dbus_start_screencast(
        self: &Rc<Self>,
        req: Start<'_>,
        reply: PendingReply<StartReply<'static>>,
    ) {
        let restore_data = match self.sc_phase.get() {
            ScreencastPhase::SourcesSelected(s) => s.restore_data.take(),
            _ => {
                self.kill();
                reply.err("Session is not in the correct phase for starting");
                return;
            }
        };
        let request_obj = match self.state.dbus.add_object(req.handle.to_string()) {
            Ok(r) => Rc::new(r),
            Err(_) => {
                self.kill();
                reply.err("Request handle is not unique");
                return;
            }
        };
        {
            use org::freedesktop::impl_::portal::request::*;
            request_obj.add_method::<Close, _>({
                let slf = self.clone();
                move |_, pr| {
                    slf.kill();
                    pr.ok(&CloseReply);
                }
            });
        }
        self.start_reply
            .set(Some(PortalSessionReply::ScreenCast(reply)));
        self.screencast_restore(&request_obj, restore_data, None);
    }

    fn start_interactive_selection(
        self: &Rc<Self>,
        request_obj: &Rc<DbusObject>,
        restore_data: Option<RestoreData>,
    ) {
        let guis = CopyHashMap::new();
        for dpy in self.state.displays.lock().values() {
            if dpy.outputs.len() > 0 {
                guis.set(dpy.id, SelectionGui::new(self, dpy, restore_data.is_some()));
            }
        }
        if guis.is_empty() {
            self.kill();
            self.reply_err("There are no running displays");
            return;
        }
        self.sc_phase
            .set(ScreencastPhase::Selecting(Rc::new(SelectingScreencast {
                core: SelectingScreencastCore {
                    session: self.clone(),
                    request_obj: request_obj.clone(),
                },
                guis,
                restore_data: Cell::new(restore_data),
            })));
    }

    pub fn screencast_restore(
        self: &Rc<Self>,
        request_obj: &Rc<DbusObject>,
        restore_data: Option<Result<RestoreData, RestoreError>>,
        display: Option<Rc<PortalDisplay>>,
    ) {
        if let Some(rd) = restore_data {
            if let Err(e) = self.try_restore(&request_obj, rd, display) {
                log::error!("Could not restore session: {}", ErrorFmt(e));
            } else {
                return;
            }
        }
        self.start_interactive_selection(&request_obj, None);
    }

    fn try_restore(
        self: &Rc<Self>,
        request_obj: &Rc<DbusObject>,
        restore_data: Result<RestoreData, RestoreError>,
        display: Option<Rc<PortalDisplay>>,
    ) -> Result<(), RestoreError> {
        let rd = restore_data?;
        let dpy = if let Some(dpy) = display {
            dpy
        } else {
            let dpy = self
                .state
                .displays
                .lock()
                .values()
                .find(|d| d.unique_id == rd.display)
                .cloned();
            match dpy {
                Some(dpy) => dpy,
                _ => {
                    if self.state.displays.len() == 0 {
                        return Err(RestoreError::UnknownDisplay);
                    } else if self.state.displays.len() == 1 {
                        self.state.displays.lock().values().next().unwrap().clone()
                    } else {
                        self.start_interactive_selection(&request_obj, Some(rd));
                        return Ok(());
                    }
                }
            }
        };
        let start = |target: ScreencastTarget| {
            SelectingScreencastCore {
                session: self.clone(),
                request_obj: request_obj.clone(),
            }
            .starting(&dpy, target);
        };
        match &rd.ty {
            RestoreDataType::Output(d) => {
                let output = dpy
                    .outputs
                    .lock()
                    .values()
                    .find(|o| o.wl.name.borrow().as_ref() == Some(&d.name))
                    .cloned();
                let Some(output) = output else {
                    return Err(RestoreError::UnknownOutput);
                };
                start(ScreencastTarget::Output(output));
            }
            RestoreDataType::Workspace(ws) => {
                let ws = dpy
                    .workspaces
                    .lock()
                    .values()
                    .find(|w| w.name.borrow().as_ref() == Some(&ws.name))
                    .cloned();
                let Some(ws) = ws else {
                    return Err(RestoreError::UnknownWorkspace);
                };
                let Some(output) = dpy.outputs.get(&ws.output.get()) else {
                    return Err(RestoreError::UnknownOutput);
                };
                start(ScreencastTarget::Workspace(output, ws, false));
            }
            RestoreDataType::Toplevel(d) => {
                if dpy.jc.version < GET_TOPLEVEL_SINCE {
                    return Err(RestoreError::GetToplevel);
                }
                let selector = dpy.jc.get_toplevel(&d.id);
                let selecting = Rc::new(SelectingWindowScreencast {
                    core: SelectingScreencastCore {
                        session: self.clone(),
                        request_obj: request_obj.clone(),
                    },
                    dpy: dpy.clone(),
                    selector: selector.clone(),
                    restoring: true,
                });
                selector.owner.set(Some(selecting.clone()));
                self.sc_phase
                    .set(ScreencastPhase::SelectingWindow(selecting));
            }
        }
        Ok(())
    }
}

impl UsrJayScreencastOwner for StartedScreencast {
    fn buffers(&self, buffers: Vec<DmaBuf>) {
        if buffers.len() == 0 {
            return;
        }
        let buffer = &buffers[0];
        *self.port.supported_formats.borrow_mut() = PwClientNodePortSupportedFormats {
            media_type: Some(SPA_MEDIA_TYPE_video),
            media_sub_type: Some(SPA_MEDIA_SUBTYPE_raw),
            video_size: Some(PwPodRectangle {
                width: buffer.width as _,
                height: buffer.height as _,
            }),
            formats: vec![PwClientNodePortSupportedFormat {
                format: buffer.format,
                modifiers: vec![buffer.modifier],
            }],
        };
        *self.port.buffer_config.borrow_mut() = PwClientNodeBufferConfig {
            num_buffers: Some(buffers.len()),
            planes: Some(buffer.planes.len()),
            data_type: SPA_DATA_DmaBuf,
        };
        self.node.send_port_update(&self.port, true);
        self.node.send_active(true);
        self.fixated.set(true);
        *self.buffers.borrow_mut() = buffers;
        self.buffers_valid.set(false);
    }

    fn ready(&self, ev: &Ready) {
        let idx = ev.idx as usize;
        let buffers = &*self.buffers.borrow();
        let pbuffers = self.port.buffers.borrow();
        let buffer = &buffers[idx];
        let discard_buffer = || {
            self.jay_screencast.release_buffer(idx);
        };
        if !self.buffers_valid.get() {
            return;
        }
        let Some(io) = self.port.io_buffers.get() else {
            discard_buffer();
            return;
        };
        let Some(pbuffer) = pbuffers.get(idx) else {
            discard_buffer();
            return;
        };
        let io = unsafe { io.read() };
        if io.status.load(Acquire) == SPA_STATUS_HAVE_DATA.0 {
            discard_buffer();
            return;
        }
        for (chunk, plane) in pbuffer.chunks.iter().zip(buffer.planes.iter()) {
            let chunk = unsafe { chunk.write() };
            chunk.flags = SpaChunkFlags::none();
            chunk.offset = plane.offset;
            chunk.stride = plane.stride;
            chunk.size = plane.stride * buffer.height as u32;
        }
        if let Some(crop) = &pbuffer.meta_video_crop {
            unsafe { crop.write() }.region = spa_region {
                position: spa_point { x: 0, y: 0 },
                size: spa_rectangle {
                    width: buffer.width as _,
                    height: buffer.height as _,
                },
            };
        }
        let buffer_id = io.buffer_id.load(Relaxed) as usize;
        if self.port_buffer_valid.get() {
            if buffer_id != idx {
                if buffer_id < buffers.len() {
                    self.jay_screencast.release_buffer(buffer_id);
                }
            }
        }
        io.buffer_id.store(ev.idx, Relaxed);
        io.status.store(SPA_STATUS_HAVE_DATA.0, Release);
        self.port_buffer_valid.set(true);
        self.port.node.drive();
    }

    fn destroyed(&self) {
        self.session.kill();
    }

    fn config(&self, config: UsrJayScreencastServerConfig) {
        let mut changed = false;
        let width = config.width.max(1);
        let height = config.height.max(1);
        changed |= self.width.replace(width) != width;
        changed |= self.height.replace(height) != height;
        self.port.supported_formats.borrow_mut().video_size = Some(PwPodRectangle {
            width: self.width.get() as _,
            height: self.height.get() as _,
        });
        if changed && self.dpy.jc.version >= CLIENT_BUFFERS_SINCE {
            self.fixated.set(false);
            init_supported_formats(&mut self.port.supported_formats.borrow_mut(), &self.dpy);
        }
        self.node.send_port_update(&self.port, self.fixated.get());
        self.node.send_active(true);
    }
}

fn init_supported_formats(
    supported_formats: &mut PwClientNodePortSupportedFormats,
    dpy: &PortalDisplay,
) {
    let Some(ctx) = dpy.render_ctx.get() else {
        return;
    };
    let Some(server_formats) = &ctx.server_formats else {
        return;
    };
    supported_formats.formats.clear();
    for format in server_formats.values() {
        if format.write_modifiers.is_empty() {
            continue;
        }
        if format.format.pipewire == SPA_VIDEO_FORMAT_UNKNOWN {
            continue;
        }
        let ptl_format = PwClientNodePortSupportedFormat {
            format: format.format,
            modifiers: format.write_modifiers.keys().copied().collect(),
        };
        supported_formats.formats.push(ptl_format);
    }
}

impl UsrLinuxBufferParamsOwner for StartedScreencast {
    fn created(&self, buffer: Rc<UsrWlBuffer>) {
        self.buffers_valid.set(true);
        self.jay_screencast.add_buffer(&buffer);
        self.jay_screencast.configure();
        self.dpy.con.remove_obj(&*buffer);
    }

    fn failed(&self) {
        log::error!("Buffer import failed");
        self.session.kill();
    }
}

pub(super) fn add_screencast_dbus_members(
    state_: &Rc<PortalState>,
    pw_con: &Rc<PwCon>,
    object: &DbusObject,
) {
    use org::freedesktop::impl_::portal::screen_cast::*;
    let state = state_.clone();
    let pw_con = pw_con.clone();
    object.add_method::<CreateSession, _>(move |req, pr| {
        dbus_create_session(&state, &pw_con, req, pr);
    });
    let state = state_.clone();
    object.add_method::<SelectSources, _>(move |req, pr| {
        dbus_select_sources(&state, req, pr);
    });
    let state = state_.clone();
    object.add_method::<Start, _>(move |req, pr| {
        dbus_start(&state, req, pr);
    });
    object.set_property::<AvailableSourceTypes>(Variant::U32(MONITOR.0));
    object.set_property::<AvailableCursorModes>(Variant::U32(EMBEDDED.0));
    object.set_property::<version>(Variant::U32(5));
}

fn dbus_create_session(
    state: &Rc<PortalState>,
    pw_con: &Rc<PwCon>,
    req: CreateSession,
    reply: PendingReply<CreateSessionReply<'static>>,
) {
    log::info!("Create Session {:#?}", req);
    if state.sessions.contains(req.session_handle.0.deref()) {
        reply.err("Session already exists");
        return;
    }
    let obj = match state.dbus.add_object(req.session_handle.0.to_string()) {
        Ok(obj) => obj,
        Err(_) => {
            reply.err("Session path is not unique");
            return;
        }
    };
    let session = Rc::new(PortalSession {
        _id: state.id(),
        state: state.clone(),
        pw_con: Some(pw_con.clone()),
        app: req.app_id.to_string(),
        session_obj: obj,
        sc_phase: CloneCell::new(ScreencastPhase::Init),
        rd_phase: CloneCell::new(RemoteDesktopPhase::Init),
        start_reply: Default::default(),
    });
    {
        use org::freedesktop::impl_::portal::session::*;
        let ses = session.clone();
        session.session_obj.add_method::<Close, _>(move |_, pr| {
            ses.kill();
            pr.ok(&SessionCloseReply);
        });
        session.session_obj.set_property::<version>(Variant::U32(4));
    }
    state
        .sessions
        .set(req.session_handle.0.to_string(), session);
    reply.ok(&CreateSessionReply {
        response: PORTAL_SUCCESS,
        results: Default::default(),
    });
}

fn dbus_select_sources(
    state: &Rc<PortalState>,
    req: SelectSources,
    reply: PendingReply<SelectSourcesReply<'static>>,
) {
    if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
        s.dbus_select_sources(req, reply);
    }
}

fn dbus_start(state: &Rc<PortalState>, req: Start, reply: PendingReply<StartReply<'static>>) {
    if let Some(s) = get_session(state, &reply, &req.session_handle.0) {
        s.dbus_start_screencast(req, reply);
    }
}

fn get_session<T>(
    state: &Rc<PortalState>,
    reply: &PendingReply<T>,
    handle: &str,
) -> Option<Rc<PortalSession>> {
    let res = state.sessions.get(handle);
    if res.is_none() {
        let msg = format!("Screencast session `{}` does not exist", handle);
        reply.err(&msg);
    }
    res
}

fn create_restore_data(dpy: &PortalDisplay, rd: &ScreencastTarget) -> Option<Variant<'static>> {
    let rd = RestoreData {
        display: dpy.unique_id,
        ty: match rd {
            ScreencastTarget::Output(o) => RestoreDataType::Output(RestoreDataOutput {
                name: o.wl.name.borrow().clone()?,
            }),
            ScreencastTarget::Workspace(_, w, _) => {
                RestoreDataType::Workspace(RestoreDataWorkspace {
                    name: w.name.borrow().clone()?,
                })
            }
            ScreencastTarget::Toplevel(tl) => RestoreDataType::Toplevel(RestoreDataToplevel {
                id: tl.toplevel_id.borrow().clone()?,
            }),
        },
    };
    Some(Variant::Struct(vec![
        Variant::String("Jay".into()),
        Variant::U32(1),
        Variant::Variant(Box::new(Variant::String(
            serde_json::to_string(&rd).unwrap().into(),
        ))),
    ]))
}

#[derive(Debug, Error)]
pub enum RestoreError {
    #[error("DBus restore data is not a struct")]
    NotAStruct,
    #[error("DBus restore data is not a struct with 3 fields")]
    NotLen3,
    #[error("DBus restore data first field is not a string")]
    FirstNotString,
    #[error("DBus restore data second field is not a u32")]
    SecondNotU32,
    #[error("DBus restore data third field is not a variant")]
    ThirdNotVariant,
    #[error("DBus restore data third field is not a string")]
    ThirdNotString,
    #[error("DBus restore data is not for Jay")]
    NotJay,
    #[error("DBus restore data is not version 1")]
    NotVersion1,
    #[error("DBus restore data could not be deserialized")]
    Parse(#[source] serde_json::Error),
    #[error("The display no longer exists")]
    UnknownDisplay,
    #[error("The output no longer exists")]
    UnknownOutput,
    #[error("The workspace no longer exists")]
    UnknownWorkspace,
    #[error("The display does not support toplevel restoration")]
    GetToplevel,
}

fn get_restore_data(req: &SelectSources) -> Option<Result<RestoreData, RestoreError>> {
    let restore_data = req.options.iter().find(|n| n.key == "restore_data")?;
    Some(get_restore_data_(restore_data))
}

fn get_restore_data_(
    restore_data: &DictEntry<Cow<str>, Variant>,
) -> Result<RestoreData, RestoreError> {
    let Variant::Struct(s) = &restore_data.value else {
        return Err(RestoreError::NotAStruct);
    };
    if s.len() != 3 {
        return Err(RestoreError::NotLen3);
    }
    let Variant::String(compositor) = &s[0] else {
        return Err(RestoreError::FirstNotString);
    };
    let Variant::U32(version) = &s[1] else {
        return Err(RestoreError::SecondNotU32);
    };
    let Variant::Variant(restore_data) = &s[2] else {
        return Err(RestoreError::ThirdNotVariant);
    };
    let Variant::String(restore_data) = &**restore_data else {
        return Err(RestoreError::ThirdNotString);
    };
    if compositor != "Jay" {
        return Err(RestoreError::NotJay);
    }
    if *version != 1 {
        return Err(RestoreError::NotVersion1);
    }
    serde_json::from_str(restore_data).map_err(RestoreError::Parse)
}

#[derive(Serialize, Deserialize)]
pub struct RestoreData {
    display: Opaque,
    ty: RestoreDataType,
}

#[derive(Serialize, Deserialize)]
enum RestoreDataType {
    Output(RestoreDataOutput),
    Workspace(RestoreDataWorkspace),
    Toplevel(RestoreDataToplevel),
}

#[derive(Serialize, Deserialize)]
struct RestoreDataOutput {
    name: String,
}

#[derive(Serialize, Deserialize)]
struct RestoreDataWorkspace {
    name: String,
}

#[derive(Serialize, Deserialize)]
struct RestoreDataToplevel {
    id: String,
}
