use core::slice;
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::{
    f32::consts::PI,
    ops::Add,
    ptr,
    sync::{atomic::AtomicU64, Arc},
    time::{Duration, Instant},
};
use vulkano::{
    command_buffer::CommandBufferUsage,
    image::{sampler::Filter, view::ImageView, Image},
    pipeline::graphics::color_blend::AttachmentBlend,
};
use wlx_capture::{
    frame::{
        DrmFormat, FrameFormat, MouseMeta, WlxFrame, DRM_FORMAT_ABGR2101010, DRM_FORMAT_ABGR8888,
        DRM_FORMAT_ARGB8888, DRM_FORMAT_XBGR2101010, DRM_FORMAT_XBGR8888, DRM_FORMAT_XRGB8888,
    },
    WlxCapture,
};

#[cfg(feature = "pipewire")]
use {
    crate::config_io,
    std::error::Error,
    std::{ops::Deref, path::PathBuf, task},
    wlx_capture::pipewire::PipewireCapture,
    wlx_capture::pipewire::PipewireSelectScreenResult,
};

#[cfg(all(feature = "x11", feature = "pipewire"))]
use wlx_capture::pipewire::PipewireStream;

#[cfg(feature = "wayland")]
use {
    crate::config::AStrMapExt,
    wlx_capture::{
        wayland::{wayland_client::protocol::wl_output, WlxClient, WlxOutput},
        wlr_dmabuf::WlrDmabufCapture,
        wlr_screencopy::WlrScreencopyCapture,
    },
};

#[cfg(feature = "x11")]
use wlx_capture::xshm::{XshmCapture, XshmScreen};

use glam::{vec2, vec3a, Affine2, Quat, Vec2, Vec3};

use crate::{
    backend::{
        input::{Haptics, InteractionHandler, PointerHit, PointerMode},
        overlay::{OverlayRenderer, OverlayState, SplitOverlayBackend},
    },
    config::{def_pw_tokens, GeneralConfig, PwTokenMap},
    graphics::{
        fourcc_to_vk, WlxCommandBuffer, WlxPipeline, WlxPipelineLegacy, DRM_FORMAT_MOD_INVALID,
    },
    hid::{MOUSE_LEFT, MOUSE_MIDDLE, MOUSE_RIGHT},
    state::{AppSession, AppState, KeyboardFocus, ScreenMeta},
};

#[cfg(feature = "wayland")]
pub(crate) type WlxClientAlias = wlx_capture::wayland::WlxClient;

#[cfg(not(feature = "wayland"))]
pub(crate) type WlxClientAlias = ();

const CURSOR_SIZE: f32 = 16. / 1440.;

static DRM_FORMATS: once_cell::sync::OnceCell<Vec<DrmFormat>> = once_cell::sync::OnceCell::new();

static START: Lazy<Instant> = Lazy::new(Instant::now);
static NEXT_MOVE: AtomicU64 = AtomicU64::new(0);

fn can_move() -> bool {
    START.elapsed().as_millis() as u64 > NEXT_MOVE.load(std::sync::atomic::Ordering::Relaxed)
}

fn set_next_move(millis_from_now: u64) {
    NEXT_MOVE.store(
        START.elapsed().as_millis() as u64 + millis_from_now,
        std::sync::atomic::Ordering::Relaxed,
    );
}

pub struct ScreenInteractionHandler {
    next_scroll: Instant,
    mouse_transform: Affine2,
}
impl ScreenInteractionHandler {
    fn new(pos: Vec2, size: Vec2, transform: Transform) -> ScreenInteractionHandler {
        let transform = match transform {
            Transform::_90 | Transform::Flipped90 => Affine2::from_cols(
                vec2(0., size.y),
                vec2(-size.x, 0.),
                vec2(pos.x + size.x, pos.y),
            ),
            Transform::_180 | Transform::Flipped180 => Affine2::from_cols(
                vec2(-size.x, 0.),
                vec2(0., -size.y),
                vec2(pos.x + size.x, pos.y + size.y),
            ),
            Transform::_270 | Transform::Flipped270 => Affine2::from_cols(
                vec2(0., -size.y),
                vec2(size.x, 0.),
                vec2(pos.x, pos.y + size.y),
            ),
            _ => Affine2::from_cols(vec2(size.x, 0.), vec2(0., size.y), pos),
        };

        ScreenInteractionHandler {
            next_scroll: Instant::now(),
            mouse_transform: transform,
        }
    }
}

impl InteractionHandler for ScreenInteractionHandler {
    fn on_hover(&mut self, app: &mut AppState, hit: &PointerHit) -> Option<Haptics> {
        #[cfg(debug_assertions)]
        log::trace!("Hover: {:?}", hit.uv);
        if can_move()
            && (!app.session.config.focus_follows_mouse_mode
                || app.input_state.pointers[hit.pointer].now.move_mouse)
        {
            let pos = self.mouse_transform.transform_point2(hit.uv);
            app.hid_provider.mouse_move(pos);
        }
        None
    }
    fn on_pointer(&mut self, app: &mut AppState, hit: &PointerHit, pressed: bool) {
        let btn = match hit.mode {
            PointerMode::Right => MOUSE_RIGHT,
            PointerMode::Middle => MOUSE_MIDDLE,
            _ => MOUSE_LEFT,
        };

        if pressed {
            set_next_move(app.session.config.click_freeze_time_ms as u64);
        }

        app.hid_provider.send_button(btn, pressed);

        if !pressed {
            return;
        }
        let pos = self.mouse_transform.transform_point2(hit.uv);
        app.hid_provider.mouse_move(pos);
    }
    fn on_scroll(&mut self, app: &mut AppState, hit: &PointerHit, delta: f32) {
        if self.next_scroll > Instant::now() {
            return;
        }
        let max_millis = if matches!(hit.mode, PointerMode::Left) {
            200.0
        } else {
            100.0
        };

        let millis = (1. - delta.abs()) * max_millis;
        self.next_scroll = Instant::now().add(Duration::from_millis(millis as _));
        app.hid_provider.wheel(if delta < 0. { -1 } else { 1 })
    }
    fn on_left(&mut self, _app: &mut AppState, _hand: usize) {}
}

#[derive(Clone)]
struct ScreenPipeline {
    view: Arc<ImageView>,
    mouse: Option<Arc<ImageView>>,
    pipeline: Arc<WlxPipeline<WlxPipelineLegacy>>,
    extentf: [f32; 2],
}

impl ScreenPipeline {
    fn new(extent: &[u32; 3], app: &mut AppState) -> anyhow::Result<ScreenPipeline> {
        let texture =
            app.graphics
                .render_texture(extent[0], extent[1], app.graphics.native_format)?;

        let view = ImageView::new_default(texture)?;

        let Ok(shaders) = app.graphics.shared_shaders.read() else {
            return Err(anyhow::anyhow!("Could not lock shared shaders for reading"));
        };

        let pipeline = app.graphics.create_pipeline(
            view.clone(),
            shaders.get("vert_common").unwrap().clone(), // want panic
            shaders.get("frag_screen").unwrap().clone(), // want panic
            app.graphics.native_format,
            Some(AttachmentBlend::default()),
        )?;

        let extentf = [extent[0] as f32, extent[1] as f32];

        Ok(ScreenPipeline {
            view,
            mouse: None,
            pipeline,
            extentf,
        })
    }

    fn ensure_mouse_initialized(&mut self, uploads: &mut WlxCommandBuffer) -> anyhow::Result<()> {
        if self.mouse.is_some() {
            return Ok(());
        }

        #[rustfmt::skip]
        let mouse_bytes = [
            0x00, 0x00, 0x00, 0xff,  0x00, 0x00, 0x00, 0xff,  0x00, 0x00, 0x00, 0xff,  0x00, 0x00, 0x00, 0xff,
            0x00, 0x00, 0x00, 0xff,  0xff, 0xff, 0xff, 0xff,  0xff, 0xff, 0xff, 0xff,  0x00, 0x00, 0x00, 0xff,
            0x00, 0x00, 0x00, 0xff,  0xff, 0xff, 0xff, 0xff,  0xff, 0xff, 0xff, 0xff,  0x00, 0x00, 0x00, 0xff,
            0x00, 0x00, 0x00, 0xff,  0x00, 0x00, 0x00, 0xff,  0x00, 0x00, 0x00, 0xff,  0x00, 0x00, 0x00, 0xff,
        ];

        let mouse_tex =
            uploads.texture2d_raw(4, 4, vulkano::format::Format::R8G8B8A8_UNORM, &mouse_bytes)?;
        self.mouse = Some(ImageView::new_default(mouse_tex)?);
        Ok(())
    }

    fn render(
        &mut self,
        image: Arc<Image>,
        mouse: Option<&MouseMeta>,
        app: &mut AppState,
    ) -> anyhow::Result<()> {
        let mut cmd = app
            .graphics
            .create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;
        let view = ImageView::new_default(image)?;
        let set0 = self
            .pipeline
            .uniform_sampler(0, view, app.graphics.texture_filtering)?;

        let pass = self.pipeline.create_pass(
            self.extentf,
            app.graphics.quad_verts.clone(),
            app.graphics.quad_indices.clone(),
            vec![set0],
        )?;

        cmd.begin_render_pass(&self.pipeline)?;
        cmd.run_ref(&pass)?;

        if let (Some(mouse), Some(mouse_view)) = (mouse, self.mouse.clone()) {
            let size = CURSOR_SIZE * self.extentf[1];
            let half_size = size * 0.5;

            let vertex_buffer = app.graphics.upload_verts(
                self.extentf[0],
                self.extentf[1],
                mouse.x * self.extentf[0] - half_size,
                mouse.y * self.extentf[1] - half_size,
                size,
                size,
            )?;

            let set0 = self
                .pipeline
                .uniform_sampler(0, mouse_view.clone(), Filter::Nearest)?;

            let pass = self.pipeline.create_pass(
                self.extentf,
                vertex_buffer,
                app.graphics.quad_indices.clone(),
                vec![set0],
            )?;

            cmd.run_ref(&pass)?;
        }

        cmd.end_render_pass()?;
        cmd.build_and_execute_now()
    }
}

pub struct ScreenRenderer {
    name: Arc<str>,
    capture: Box<dyn WlxCapture>,
    pipeline: Option<ScreenPipeline>,
    last_view: Option<Arc<ImageView>>,
    extent: Option<[u32; 3]>,
}

impl ScreenRenderer {
    #[cfg(feature = "wayland")]
    pub fn new_raw(name: Arc<str>, capture: Box<dyn WlxCapture>) -> ScreenRenderer {
        ScreenRenderer {
            name,
            capture,
            pipeline: None,
            last_view: None,
            extent: None,
        }
    }

    #[cfg(feature = "wayland")]
    pub fn new_wlr_dmabuf(output: &WlxOutput) -> Option<ScreenRenderer> {
        let client = WlxClient::new()?;
        let capture = WlrDmabufCapture::new(client, output.id);

        Some(ScreenRenderer {
            name: output.name.clone(),
            capture: Box::new(capture),
            pipeline: None,
            last_view: None,
            extent: None,
        })
    }

    #[cfg(feature = "wayland")]
    pub fn new_wlr_screencopy(output: &WlxOutput) -> Option<ScreenRenderer> {
        let client = WlxClient::new()?;
        let capture = WlrScreencopyCapture::new(client, output.id);

        Some(ScreenRenderer {
            name: output.name.clone(),
            capture: Box::new(capture),
            pipeline: None,
            last_view: None,
            extent: None,
        })
    }

    #[cfg(feature = "wayland")]
    pub fn new_pw(
        output: &WlxOutput,
        token: Option<&str>,
        session: &AppSession,
    ) -> anyhow::Result<(
        ScreenRenderer,
        Option<String>, /* pipewire restore token */
    )> {
        let name = output.name.clone();
        let embed_mouse = !session.config.double_cursor_fix;

        let select_screen_result = select_pw_screen(
            &format!(
                "Now select: {} {} {} @ {},{}",
                &output.name,
                &output.make,
                &output.model,
                &output.logical_pos.0,
                &output.logical_pos.1
            ),
            token,
            embed_mouse,
            true,
            true,
            false,
        )?;

        let node_id = select_screen_result.streams.first().unwrap().node_id; // streams guaranteed to have at least one element

        let capture = PipewireCapture::new(name, node_id);

        Ok((
            ScreenRenderer {
                name: output.name.clone(),
                capture: Box::new(capture),
                pipeline: None,
                last_view: None,
                extent: None,
            },
            select_screen_result.restore_token,
        ))
    }

    #[cfg(feature = "x11")]
    pub fn new_xshm(screen: Arc<XshmScreen>) -> ScreenRenderer {
        let capture = XshmCapture::new(screen.clone());

        ScreenRenderer {
            name: screen.name.clone(),
            capture: Box::new(capture),
            pipeline: None,
            last_view: None,
            extent: None,
        }
    }
}

impl OverlayRenderer for ScreenRenderer {
    fn init(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
        Ok(())
    }
    fn render(&mut self, app: &mut AppState) -> anyhow::Result<()> {
        if !self.capture.is_ready() {
            let supports_dmabuf = app
                .graphics
                .device
                .enabled_extensions()
                .ext_external_memory_dma_buf
                && self.capture.supports_dmbuf();

            let allow_dmabuf = &*app.session.config.capture_method != "pw_fallback"
                && &*app.session.config.capture_method != "screencopy";

            let capture_method = app.session.config.capture_method.clone();

            let drm_formats = DRM_FORMATS.get_or_init({
                let graphics = app.graphics.clone();
                move || {
                    if !supports_dmabuf {
                        log::info!("Capture method does not support DMA-buf");
                        return vec![];
                    }
                    if !allow_dmabuf {
                        log::info!("Not using DMA-buf capture due to {}", capture_method);
                        return vec![];
                    }
                    log::warn!("Using DMA-buf capture. If screens are blank for you, switch to SHM using:");
                    log::warn!("echo 'capture_method: pw_fallback' > ~/.config/wlxoverlay/conf.d/pw_fallback.yaml");

                    let possible_formats = [
                        DRM_FORMAT_ABGR8888.into(),
                        DRM_FORMAT_XBGR8888.into(),
                        DRM_FORMAT_ARGB8888.into(),
                        DRM_FORMAT_XRGB8888.into(),
                        DRM_FORMAT_ABGR2101010.into(),
                        DRM_FORMAT_XBGR2101010.into(),
                    ];

                    let mut final_formats = vec![];

                    for &f in &possible_formats {
                        let Ok(vk_fmt) = fourcc_to_vk(f) else {
                            continue;
                        };
                        let Ok(props) = graphics.device.physical_device().format_properties(vk_fmt)
                        else {
                            continue;
                        };
                        let mut fmt = DrmFormat {
                            fourcc: f,
                            modifiers: props
                                .drm_format_modifier_properties
                                .iter()
                                // important bit: only allow single-plane
                                .filter(|m| m.drm_format_modifier_plane_count == 1)
                                .map(|m| m.drm_format_modifier)
                                .collect(),
                        };
                        fmt.modifiers.push(DRM_FORMAT_MOD_INVALID); // implicit modifiers support
                        final_formats.push(fmt);
                    }
                    log::debug!("Supported DRM formats:");
                    for f in &final_formats {
                        log::debug!("  {} {:?}", f.fourcc, f.modifiers);
                    }
                    final_formats
                }
            });

            self.capture.init(drm_formats);
            self.capture.request_new_frame();
        };

        for frame in self.capture.receive().into_iter() {
            match frame {
                WlxFrame::Dmabuf(frame) => {
                    if !frame.is_valid() {
                        log::error!("Invalid frame");
                        continue;
                    }
                    self.extent.get_or_insert_with(|| {
                        extent_from_format(frame.format, &app.session.config)
                    });
                    match app.graphics.dmabuf_texture(frame) {
                        Ok(new) => {
                            let pipeline = match self.pipeline {
                                Some(ref mut p) => Some(p),
                                None if app.session.config.screen_render_down => {
                                    log::info!("{}: Using render-down pass.", self.name);
                                    let pipeline = ScreenPipeline::new(&self.extent.unwrap(), app)?; // safe
                                    self.last_view = Some(pipeline.view.clone());
                                    self.pipeline = Some(pipeline);
                                    self.pipeline.as_mut()
                                }
                                None => None,
                            };
                            if let Some(pipeline) = pipeline {
                                pipeline.render(new.clone(), None, app)?;
                            } else {
                                let view = ImageView::new_default(new.clone())?;
                                self.last_view = Some(view);
                            }
                        }
                        Err(e) => {
                            log::error!(
                                "{}: Failed to create DMA-buf texture: {}",
                                self.name,
                                e.to_string()
                            );
                        }
                    }
                    self.capture.request_new_frame();
                }
                WlxFrame::MemFd(frame) => {
                    let mut upload = app
                        .graphics
                        .create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;

                    let Some(fd) = frame.plane.fd else {
                        log::error!("No fd");
                        continue;
                    };
                    self.extent.get_or_insert_with(|| {
                        extent_from_format(frame.format, &app.session.config)
                    });
                    log::debug!("{}: New MemFd frame", self.name);
                    let format = fourcc_to_vk(frame.format.fourcc)?;

                    let len = frame.plane.stride as usize * frame.format.height as usize;
                    let offset = frame.plane.offset as i64;

                    let map = unsafe {
                        libc::mmap(
                            ptr::null_mut(),
                            len,
                            libc::PROT_READ,
                            libc::MAP_SHARED,
                            fd,
                            offset,
                        )
                    } as *const u8;

                    let data = unsafe { slice::from_raw_parts(map, len) };

                    let image = upload.texture2d_raw(
                        frame.format.width,
                        frame.format.height,
                        format,
                        data,
                    )?;
                    upload.build_and_execute_now()?;

                    unsafe { libc::munmap(map as *mut _, len) };

                    self.last_view = Some(ImageView::new_default(image)?);
                    self.capture.request_new_frame();
                }
                WlxFrame::MemPtr(frame) => {
                    log::debug!("{}: New MemPtr frame", self.name);
                    let mut upload = app
                        .graphics
                        .create_command_buffer(CommandBufferUsage::OneTimeSubmit)?;

                    let format = fourcc_to_vk(frame.format.fourcc)?;

                    let data = unsafe { slice::from_raw_parts(frame.ptr as *const u8, frame.size) };

                    let image = upload.texture2d_raw(
                        frame.format.width,
                        frame.format.height,
                        format,
                        data,
                    )?;

                    let pipeline = Some(match self.pipeline {
                        Some(ref mut p) => p,
                        _ => {
                            log::info!("{}: Using render-down pass.", self.name);
                            let extent = extent_from_format(frame.format, &app.session.config);
                            let mut pipeline = ScreenPipeline::new(&extent, app)?;
                            self.last_view = Some(pipeline.view.clone());
                            pipeline.ensure_mouse_initialized(&mut upload)?;
                            self.pipeline = Some(pipeline);
                            self.extent = Some(extent);
                            self.pipeline.as_mut().unwrap() // safe
                        }
                    });

                    upload.build_and_execute_now()?;

                    if let Some(pipeline) = pipeline {
                        pipeline.render(image, frame.mouse.as_ref(), app)?;
                    } else {
                        let view = ImageView::new_default(image)?;
                        self.last_view = Some(view);
                    }
                    self.capture.request_new_frame();
                }
            };
        }
        Ok(())
    }
    fn pause(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
        self.capture.pause();
        Ok(())
    }
    fn resume(&mut self, _app: &mut AppState) -> anyhow::Result<()> {
        self.capture.resume();
        Ok(())
    }
    fn view(&mut self) -> Option<Arc<ImageView>> {
        self.last_view.clone()
    }
    fn extent(&mut self) -> Option<[u32; 3]> {
        self.extent
    }
}

#[cfg(feature = "wayland")]
pub fn create_screen_renderer_wl(
    output: &WlxOutput,
    has_wlr_dmabuf: bool,
    has_wlr_screencopy: bool,
    pw_token_store: &mut PwTokenMap,
    session: &AppSession,
) -> Option<ScreenRenderer> {
    let mut capture: Option<ScreenRenderer> = None;
    if (&*session.config.capture_method == "wlr-dmabuf") && has_wlr_dmabuf {
        log::info!("{}: Using Wlr DMA-Buf", &output.name);
        capture = ScreenRenderer::new_wlr_dmabuf(output);
    }

    if &*session.config.capture_method == "screencopy" && has_wlr_screencopy {
        log::info!("{}: Using Wlr Screencopy Wl-SHM", &output.name);
        capture = ScreenRenderer::new_wlr_screencopy(output);
    }

    if capture.is_none() {
        log::info!("{}: Using Pipewire capture", &output.name);

        let display_name = output.name.deref();

        // Find existing token by display
        let token = pw_token_store.arc_get(display_name).map(|s| s.as_str());

        if let Some(t) = token {
            log::info!(
                "Found existing Pipewire token for display {}: {}",
                display_name,
                t
            );
        }

        match ScreenRenderer::new_pw(output, token, session) {
            Ok((renderer, restore_token)) => {
                capture = Some(renderer);

                if let Some(token) = restore_token {
                    if pw_token_store.arc_set(display_name.into(), token.clone()) {
                        log::info!("Adding Pipewire token {}", token);
                    }
                }
            }
            Err(e) => {
                log::warn!(
                    "{}: Failed to create Pipewire capture: {:?}",
                    &output.name,
                    e
                );
            }
        }
    }
    capture
}

pub fn create_screen_interaction(
    logical_pos: Vec2,
    logical_size: Vec2,
    transform: Transform,
) -> ScreenInteractionHandler {
    ScreenInteractionHandler::new(logical_pos, logical_size, transform)
}

fn create_screen_state(
    name: Arc<str>,
    res: (i32, i32),
    transform: Transform,
    session: &AppSession,
) -> OverlayState {
    let angle = if session.config.upright_screen_fix {
        match transform {
            Transform::_90 | Transform::Flipped90 => PI / 2.,
            Transform::_180 | Transform::Flipped180 => PI,
            Transform::_270 | Transform::Flipped270 => -PI / 2.,
            _ => 0.,
        }
    } else {
        0.
    };

    let center = Vec2 { x: 0.5, y: 0.5 };
    let interaction_transform = match transform {
        Transform::_90 | Transform::Flipped90 => Affine2::from_cols(
            Vec2::NEG_Y * (res.0 as f32 / res.1 as f32),
            Vec2::NEG_X,
            center,
        ),
        Transform::_180 | Transform::Flipped180 => Affine2::from_cols(
            Vec2::NEG_X,
            Vec2::NEG_Y * (-res.0 as f32 / res.1 as f32),
            center,
        ),
        Transform::_270 | Transform::Flipped270 => {
            Affine2::from_cols(Vec2::Y * (res.0 as f32 / res.1 as f32), Vec2::X, center)
        }
        _ if res.1 > res.0 => {
            // Xorg upright screens
            Affine2::from_cols(Vec2::X * (res.1 as f32 / res.0 as f32), Vec2::NEG_Y, center)
        }
        _ => Affine2::from_cols(Vec2::X, Vec2::NEG_Y * (res.0 as f32 / res.1 as f32), center),
    };

    OverlayState {
        name: name.clone(),
        keyboard_focus: Some(KeyboardFocus::PhysicalScreen),
        grabbable: true,
        recenter: true,
        anchored: true,
        interactable: true,
        spawn_scale: 1.5 * session.config.desktop_view_scale,
        spawn_point: vec3a(0., 0.5, 0.),
        spawn_rotation: Quat::from_axis_angle(Vec3::Z, angle),
        interaction_transform,
        ..Default::default()
    }
}

#[derive(Deserialize, Serialize, Default)]
pub struct TokenConf {
    #[serde(default = "def_pw_tokens")]
    pub pw_tokens: PwTokenMap,
}

#[cfg(feature = "pipewire")]
fn get_pw_token_path() -> PathBuf {
    let mut path = config_io::get_conf_d_path();
    path.push("pw_tokens.yaml");
    path
}

#[cfg(feature = "pipewire")]
pub fn save_pw_token_config(tokens: PwTokenMap) -> Result<(), Box<dyn Error>> {
    let conf = TokenConf { pw_tokens: tokens };
    let yaml = serde_yaml::to_string(&conf)?;
    std::fs::write(get_pw_token_path(), yaml)?;

    Ok(())
}

#[cfg(feature = "pipewire")]
pub fn load_pw_token_config() -> Result<PwTokenMap, Box<dyn Error>> {
    let yaml = std::fs::read_to_string(get_pw_token_path())?;
    let conf: TokenConf = serde_yaml::from_str(yaml.as_str())?;
    Ok(conf.pw_tokens)
}

pub(crate) struct ScreenCreateData {
    pub screens: Vec<(ScreenMeta, OverlayState, Box<SplitOverlayBackend>)>,
}

#[cfg(not(feature = "wayland"))]
pub fn create_screens_wayland(
    _wl: &mut WlxClientAlias,
    _app: &AppState,
) -> anyhow::Result<ScreenCreateData> {
    anyhow::bail!("Wayland support not enabled")
}

#[cfg(feature = "wayland")]
pub fn create_screens_wayland(
    wl: &mut WlxClientAlias,
    app: &mut AppState,
) -> anyhow::Result<ScreenCreateData> {
    use crate::config::AStrMap;

    let mut screens = vec![];

    // Load existing Pipewire tokens from file
    let mut pw_tokens: PwTokenMap = if let Ok(conf) = load_pw_token_config() {
        conf
    } else {
        AStrMap::new()
    };

    let pw_tokens_copy = pw_tokens.clone();
    let has_wlr_dmabuf = wl.maybe_wlr_dmabuf_mgr.is_some();
    let has_wlr_screencopy = wl.maybe_wlr_screencopy_mgr.is_some();

    for (id, output) in wl.outputs.iter() {
        if app.screens.iter().any(|s| s.name == output.name) {
            continue;
        }

        log::info!(
            "{}: Init screen of res {:?}, logical {:?} at {:?}",
            output.name,
            output.size,
            output.logical_size,
            output.logical_pos,
        );

        if let Some(renderer) = create_screen_renderer_wl(
            output,
            has_wlr_dmabuf,
            has_wlr_screencopy,
            &mut pw_tokens,
            &app.session,
        ) {
            let logical_pos = vec2(output.logical_pos.0 as f32, output.logical_pos.1 as f32);
            let logical_size = vec2(output.logical_size.0 as f32, output.logical_size.1 as f32);
            let transform = output.transform.into();
            let interaction = create_screen_interaction(logical_pos, logical_size, transform);
            let state =
                create_screen_state(output.name.clone(), output.size, transform, &app.session);

            let meta = ScreenMeta {
                name: wl.outputs[id].name.clone(),
                id: state.id,
                native_handle: *id,
            };

            let backend = Box::new(SplitOverlayBackend {
                renderer: Box::new(renderer),
                interaction: Box::new(interaction),
            });
            screens.push((meta, state, backend));
        }
    }

    if pw_tokens_copy != pw_tokens {
        // Token list changed, re-create token config file
        if let Err(err) = save_pw_token_config(pw_tokens) {
            log::error!("Failed to save Pipewire token config: {}", err);
        }
    }

    let extent = wl.get_desktop_extent();
    let origin = wl.get_desktop_origin();

    app.hid_provider
        .set_desktop_extent(vec2(extent.0 as f32, extent.1 as f32));
    app.hid_provider
        .set_desktop_origin(vec2(origin.0 as f32, origin.1 as f32));

    Ok(ScreenCreateData { screens })
}

#[cfg(not(feature = "x11"))]
pub fn create_screens_xshm(_app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
    anyhow::bail!("X11 support not enabled")
}

#[cfg(not(all(feature = "x11", feature = "pipewire")))]
pub fn create_screens_x11pw(_app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
    anyhow::bail!("Pipewire support not enabled")
}

#[cfg(all(feature = "x11", feature = "pipewire"))]
pub fn create_screens_x11pw(app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
    use crate::config::{AStrMap, AStrMapExt};
    use anyhow::bail;

    // Load existing Pipewire tokens from file
    let mut pw_tokens: PwTokenMap = if let Ok(conf) = load_pw_token_config() {
        conf
    } else {
        AStrMap::new()
    };
    let pw_tokens_copy = pw_tokens.clone();
    let token = pw_tokens.arc_get("x11").map(|s| s.as_str());
    let embed_mouse = !app.session.config.double_cursor_fix;

    let select_screen_result = select_pw_screen(
        "Select ALL screens on the screencast pop-up!",
        token,
        embed_mouse,
        true,
        true,
        true,
    )?;

    if let Some(restore_token) = select_screen_result.restore_token {
        if pw_tokens.arc_set("x11".into(), restore_token.clone()) {
            log::info!("Adding Pipewire token {}", restore_token);
        }
    }
    if pw_tokens_copy != pw_tokens {
        // Token list changed, re-create token config file
        if let Err(err) = save_pw_token_config(pw_tokens) {
            log::error!("Failed to save Pipewire token config: {}", err);
        }
    }

    let monitors = match XshmCapture::get_monitors() {
        Ok(m) => m,
        Err(e) => {
            bail!(e.to_string());
        }
    };
    log::info!("Got {} monitors", monitors.len());
    log::info!("Got {} streams", select_screen_result.streams.len());

    let mut extent = vec2(0., 0.);
    let screens = select_screen_result
        .streams
        .into_iter()
        .enumerate()
        .map(|(i, s)| {
            let m = best_match(&s, monitors.iter().map(AsRef::as_ref)).unwrap();
            log::info!("Stream {i} is {}", m.name);
            extent.x = extent.x.max((m.monitor.x() + m.monitor.width()) as f32);
            extent.y = extent.y.max((m.monitor.y() + m.monitor.height()) as f32);

            let size = (m.monitor.width(), m.monitor.height());
            let interaction = create_screen_interaction(
                vec2(m.monitor.x() as f32, m.monitor.y() as f32),
                vec2(m.monitor.width() as f32, m.monitor.height() as f32),
                Transform::Normal,
            );

            let state = create_screen_state(m.name.clone(), size, Transform::Normal, &app.session);

            let meta = ScreenMeta {
                name: m.name.clone(),
                id: state.id,
                native_handle: 0,
            };

            let renderer = ScreenRenderer {
                name: m.name.clone(),
                capture: Box::new(PipewireCapture::new(m.name.clone(), s.node_id)),
                pipeline: None,
                last_view: None,
                extent: Some(extent_from_res(
                    size.0 as _,
                    size.1 as _,
                    &app.session.config,
                )),
            };

            let backend = Box::new(SplitOverlayBackend {
                renderer: Box::new(renderer),
                interaction: Box::new(interaction),
            });
            (meta, state, backend)
        })
        .collect();

    app.hid_provider.set_desktop_extent(extent);
    app.hid_provider.set_desktop_origin(vec2(0.0, 0.0));

    Ok(ScreenCreateData { screens })
}

#[cfg(feature = "x11")]
pub fn create_screens_xshm(app: &mut AppState) -> anyhow::Result<ScreenCreateData> {
    use anyhow::bail;

    let mut extent = vec2(0., 0.);

    let monitors = match XshmCapture::get_monitors() {
        Ok(m) => m,
        Err(e) => {
            bail!(e.to_string());
        }
    };

    let screens = monitors
        .into_iter()
        .map(|s| {
            extent.x = extent.x.max((s.monitor.x() + s.monitor.width()) as f32);
            extent.y = extent.y.max((s.monitor.y() + s.monitor.height()) as f32);

            let size = (s.monitor.width(), s.monitor.height());
            let pos = (s.monitor.x(), s.monitor.y());
            let renderer = ScreenRenderer::new_xshm(s.clone());

            log::info!(
                "{}: Init X11 screen of res {:?} at {:?}",
                s.name.clone(),
                size,
                pos,
            );

            let interaction = create_screen_interaction(
                vec2(s.monitor.x() as f32, s.monitor.y() as f32),
                vec2(size.0 as f32, size.1 as f32),
                Transform::Normal,
            );

            let state = create_screen_state(s.name.clone(), size, Transform::Normal, &app.session);

            let meta = ScreenMeta {
                name: s.name.clone(),
                id: state.id,
                native_handle: 0,
            };

            let backend = Box::new(SplitOverlayBackend {
                renderer: Box::new(renderer),
                interaction: Box::new(interaction),
            });
            (meta, state, backend)
        })
        .collect();

    app.hid_provider.set_desktop_extent(extent);
    app.hid_provider.set_desktop_origin(vec2(0.0, 0.0));

    Ok(ScreenCreateData { screens })
}

#[allow(unused)]
#[derive(Clone, Copy)]
pub enum Transform {
    Normal,
    _90,
    _180,
    _270,
    Flipped90,
    Flipped180,
    Flipped270,
}

#[cfg(feature = "wayland")]
impl From<wl_output::Transform> for Transform {
    fn from(t: wl_output::Transform) -> Transform {
        match t {
            wl_output::Transform::Normal => Transform::Normal,
            wl_output::Transform::_90 => Transform::_90,
            wl_output::Transform::_180 => Transform::_180,
            wl_output::Transform::_270 => Transform::_270,
            wl_output::Transform::Flipped => Transform::Flipped180,
            wl_output::Transform::Flipped90 => Transform::Flipped90,
            wl_output::Transform::Flipped180 => Transform::Flipped180,
            wl_output::Transform::Flipped270 => Transform::Flipped270,
            _ => Transform::Normal,
        }
    }
}

fn extent_from_format(fmt: FrameFormat, config: &GeneralConfig) -> [u32; 3] {
    extent_from_res(fmt.width, fmt.height, config)
}

fn extent_from_res(width: u32, height: u32, config: &GeneralConfig) -> [u32; 3] {
    // screens above a certain resolution will have severe aliasing
    let h = height.min(config.screen_max_height as u32);
    let w = (width as f32 / height as f32 * h as f32) as u32;
    [w, h, 1]
}

#[cfg(all(feature = "pipewire", feature = "x11"))]
fn best_match<'a>(
    stream: &PipewireStream,
    mut streams: impl Iterator<Item = &'a XshmScreen>,
) -> Option<&'a XshmScreen> {
    let mut best = streams.next();
    log::debug!("stream: {:?}", stream.position);
    log::debug!("first: {:?}", best.map(|b| &b.monitor));
    let Some(position) = stream.position else {
        return best;
    };

    let mut best_dist = best
        .map(|b| (b.monitor.x() - position.0).abs() + (b.monitor.y() - position.1).abs())
        .unwrap_or(i32::MAX);
    for stream in streams {
        log::debug!("checking: {:?}", stream.monitor);
        let dist =
            (stream.monitor.x() - position.0).abs() + (stream.monitor.y() - position.1).abs();
        if dist < best_dist {
            best = Some(stream);
            best_dist = dist;
        }
    }
    log::debug!("best: {:?}", best.map(|b| &b.monitor));
    best
}

#[cfg(feature = "pipewire")]
fn select_pw_screen(
    instructions: &str,
    token: Option<&str>,
    embed_mouse: bool,
    screens_only: bool,
    persist: bool,
    multiple: bool,
) -> Result<PipewireSelectScreenResult, wlx_capture::pipewire::AshpdError> {
    use crate::backend::notifications::DbusNotificationSender;
    use wlx_capture::pipewire::pipewire_select_screen;

    let future = async move {
        let print_at = Instant::now() + Duration::from_millis(250);
        let mut notify = None;

        let f = pipewire_select_screen(token, embed_mouse, screens_only, persist, multiple);
        futures::pin_mut!(f);

        loop {
            match futures::poll!(&mut f) {
                task::Poll::Ready(result) => return result,
                task::Poll::Pending => {
                    if Instant::now() >= print_at {
                        log::info!("{}", instructions);
                        if let Ok(sender) = DbusNotificationSender::new() {
                            if let Ok(id) = sender.notify_send(instructions, "", 2, 0, 0, true) {
                                notify = Some((sender, id));
                            }
                        }
                        break;
                    }
                    futures::future::lazy(|_| {
                        std::thread::sleep(Duration::from_millis(10));
                    })
                    .await;
                    continue;
                }
            }
        }

        let result = f.await;
        if let Some((sender, id)) = notify {
            let _ = sender.notify_close(id);
        }
        result
    };

    futures::executor::block_on(future)
}
