// X11Surface registry + render / input integration.
//
// Forge's primary window machinery (`windows::Layouts`, `windows::Window<S>`)
// is keyed on `WlSurface` + an xdg/layer/lock state — an X11Surface carries
// neither. This module is the parallel registry that lets XWayland-backed
// clients participate in the desktop alongside Wayland toplevels without
// having to retrofit the X11Surface into the `Window<S>` enum.
//
// As of the eydos #4 first-class parity pass (commit 20b7500) it covers:
//   - Map / unmap / destroy lifecycle (XwmHandler hooks).
//   - Buffer import per frame from the wl_surface tree.
//   - Geometry: `configure_request` x/y/w/h, including the top-y clamp
//     against `Output::available()` so the SSD bar never paints over
//     the topbar.
//   - SSD title bar with the same `SsdDecoration` instance the Wayland
//     side uses, painted by `paint_one` / `paint_all`.
//   - Move / resize drag — parallel `MoveDragX11` / `ResizeDragX11`
//     mirrors of the Wayland flow in `Windows`.
//   - Input routing through `Windows::surface_at` (`X11(WlSurface)`
//     variant on `InputSurfaceKind`).
//   - Z-order coordination with Wayland toplevels via
//     `Windows::toplevel_z_order`.
//   - `ext-foreign-toplevel-list-v1` advertising → host shell dock /
//     Alt+Tab inclusion (the latter via `Windows::app_switcher_entries`,
//     which appends a `WindowEntry` per non-OR X11 surface).
//   - `XWaylandShellHandler::surface_associated` promotes the wl_surface
//     on late xwayland association so clients that map before XWayland
//     binds don't end up orphaned.
//
// Snap zones are deliberately Wayland-only — X11 rarely wants edge
// tiling and the existing `SnapZone` machinery is keyed off `Window<S>`.
// Geometry persistence (eydos #7) is also Wayland-only because the
// Wayland `app_id` is the persistence key and X11 carries `class` /
// `instance` instead.

use smithay::backend::renderer::gles::GlesRenderer;
use smithay::backend::renderer::ImportAll;
use smithay::reexports::wayland_server::protocol::wl_surface::WlSurface;
use smithay::utils::{Logical, Physical, Point, Rectangle, Size};
use smithay::wayland::compositor::{self, TraversalAction};
use smithay::wayland::single_pixel_buffer;
use smithay::xwayland::xwm::WmWindowType;
use smithay::xwayland::X11Surface;

use std::cell::RefCell;

use crate::drawing::{ForgeElement, ForgeSurfaceData, RenderTexture, Texture};
use smithay::wayland::foreign_toplevel_list::ForeignToplevelHandle;

#[derive(Debug)]
pub struct X11WindowEntry {
    /// The XWayland-side surface handle. Owns geometry, mapped state, etc.
    pub surface:       X11Surface,
    /// Cached imported textures from the last `import_buffers` pass.
    /// `RenderTexture` is `Rc<Texture>`, so cloning these into the output
    /// element list is a refcount bump and the GL handles outlive a single
    /// frame.
    texture_cache:     Vec<(RenderTexture, Point<i32, Logical>)>,
    pub minimized:     bool,
    saved_geo:         Option<Rectangle<i32, Logical>>,
    foreign_handle:    Option<ForeignToplevelHandle>,
    ssd:               Option<crate::ssd::SsdDecoration>,
}

impl X11WindowEntry {
    fn largest_texture(&self) -> Option<&RenderTexture> {
        self.texture_cache
            .iter()
            .map(|(t, _)| t)
            .max_by_key(|t| {
                let s = t.size();
                (s.w as i64) * (s.h as i64)
            })
    }

    pub fn thumbnail_texture(&self) -> Option<RenderTexture> {
        self.largest_texture().cloned()
    }

    pub fn thumbnail_size(&self) -> Option<Size<i32, Logical>> {
        self.largest_texture().map(|t| t.size())
    }
}

/// True for X11 surfaces that should never carry an SSD title bar:
/// override-redirect popups (menus, tooltips, drag icons) plus the
/// `_NET_WM_WINDOW_TYPE` hints whose semantics are "no chrome" — most
/// notably `Splash`, which LibreOffice and several Qt apps use for
/// their startup window.
pub fn is_undecorated(surface: &X11Surface) -> bool {
    if surface.is_override_redirect() { return true; }
    matches!(
        surface.window_type(),
        Some(
            WmWindowType::Splash
            | WmWindowType::Tooltip
            | WmWindowType::Notification
            | WmWindowType::Menu
            | WmWindowType::DropdownMenu
            | WmWindowType::PopupMenu
        ),
    )
}

pub fn wants_ssd(surface: &X11Surface) -> bool {
    !is_undecorated(surface)
}

#[derive(Debug, Default)]
pub struct X11Windows {
    entries: Vec<X11WindowEntry>,
}

impl X11Windows {
    pub fn new() -> Self {
        Self { entries: Vec::new() }
    }

    pub fn is_empty(&self) -> bool {
        self.entries.is_empty()
    }

    pub fn entries(&self) -> &[X11WindowEntry] {
        &self.entries
    }

    /// Register a newly mapped X11Surface. No-op if `surface` is already
    /// tracked — callers can re-emit `map_window_request` without breaking
    /// the registry.
    pub fn map(&mut self, surface: X11Surface, foreign_handle: Option<ForeignToplevelHandle>) {
        if self.entries.iter().any(|e| e.surface == surface) { return; }
        self.entries.push(X11WindowEntry {
            surface,
            texture_cache: Vec::new(),
            minimized: false,
            saved_geo: None,
            foreign_handle,
            ssd: None,
        });
    }

    /// Drop an X11Surface from the registry — both `unmapped_window` and
    /// `destroyed_window` route here.
    pub fn unmap(&mut self, surface: &X11Surface) {
        if let Some(idx) = self.entries.iter().position(|e| &e.surface == surface) {
            let entry = self.entries.remove(idx);
            if let Some(h) = entry.foreign_handle { h.send_closed(); }
        }
    }

    pub fn update_title(&self, surface: &X11Surface, title: &str) {
        for e in &self.entries {
            if &e.surface == surface {
                if let Some(h) = &e.foreign_handle {
                    h.send_title(title);
                    h.send_done();
                }
                return;
            }
        }
    }

    pub fn update_app_id(&self, surface: &X11Surface, app_id: &str) {
        for e in &self.entries {
            if &e.surface == surface {
                if let Some(h) = &e.foreign_handle {
                    h.send_app_id(app_id);
                    h.send_done();
                }
                return;
            }
        }
    }

    /// True when any tracked X11Surface roots `wl_surface`. Used by
    /// `Windows::surface_commit` so an XWayland-backed wl_surface commit
    /// gets routed into the X11 pipeline instead of falling off the end.
    pub fn owns(&self, wl_surface: &WlSurface) -> bool {
        self.entries.iter().any(|e| {
            e.surface
                .wl_surface()
                .as_ref()
                .map_or(false, |s| s == wl_surface)
        })
    }

    pub fn find(&self, wl_surface: &WlSurface) -> Option<&X11Surface> {
        self.entries.iter()
            .find(|e| e.surface.wl_surface().as_ref().map_or(false, |s| s == wl_surface))
            .map(|e| &e.surface)
    }

    pub fn surface_commit(&mut self, surface: &WlSurface) {
        compositor::with_surface_tree_upward(
            surface,
            (),
            |_, data, _| {
                let mut surface_data = data
                    .data_map
                    .get_or_insert(|| RefCell::new(ForgeSurfaceData::new()))
                    .borrow_mut();

                let mut attrs = data.cached_state.get::<smithay::wayland::compositor::SurfaceAttributes>();
                let attrs = attrs.current();
                let Some(buffer_assignment) = attrs.buffer.take() else {
                    return TraversalAction::DoChildren(());
                };

                surface_data.opaque_region = attrs
                    .opaque_region
                    .as_mut()
                    .map(|r| std::mem::take(&mut r.rects))
                    .unwrap_or_default();

                surface_data.update_buffer(data, attrs, buffer_assignment);

                TraversalAction::DoChildren(())
            },
            |_, _, _| (),
            |_, _, _| true,
        );
    }

    /// Entries sorted topmost-first by the global toplevel z-order.
    /// Entries whose wl_surface is missing or not yet promoted sort
    /// below the ordered ones, keeping reverse-map order among
    /// themselves.
    fn entries_top_down(&self, z_order: &[WlSurface]) -> Vec<&X11WindowEntry> {
        let mut sorted: Vec<(Option<usize>, &X11WindowEntry)> = self
            .entries
            .iter()
            .rev()
            .map(|e| {
                let z = e
                    .surface
                    .wl_surface()
                    .and_then(|wl| z_order.iter().position(|s| s == &wl));
                (z, e)
            })
            .collect();
        sorted.sort_by(|a, b| b.0.cmp(&a.0));
        sorted.into_iter().map(|(_, e)| e).collect()
    }

    /// Hit-test from a screen-space logical position. Walks entries
    /// topmost-first and returns the surface plus its on-screen origin
    /// for the first one whose geometry contains `position`.
    pub fn surface_at(
        &self,
        position: Point<f64, Logical>,
        z_order: &[WlSurface],
    ) -> Option<(WlSurface, Point<f64, Logical>)> {
        for entry in self.entries_top_down(z_order) {
            if !entry.surface.is_mapped() || entry.minimized { continue; }
            let geo = entry.surface.geometry();
            let geo_f = geo.to_f64();
            if !geo_f.contains(position) { continue; }
            let wl = entry.surface.wl_surface()?;
            return Some((wl.clone(), geo_f.loc));
        }
        None
    }

    pub fn resize_hit_test(
        &self,
        point: Point<f64, Logical>,
        z_order: &[WlSurface],
    ) -> Option<(WlSurface, crate::ssd::ResizeEdge, Rectangle<i32, Logical>)> {
        let bar_h = crate::ssd::SsdDecoration::bar_height();
        for entry in self.entries_top_down(z_order) {
            if !entry.surface.is_mapped() || entry.minimized { continue; }
            if is_undecorated(&entry.surface) { continue; }
            let geo = entry.surface.geometry();
            if crate::ssd::body_hit_test(geo, point, bar_h) { return None; }
            if let Some(edge) = crate::ssd::resize_hit_test(geo, point, bar_h) {
                let wl = entry.surface.wl_surface()?;
                return Some((wl, edge, geo));
            }
        }
        None
    }

    pub fn ssd_hit_test(
        &self,
        point: Point<f64, Logical>,
        z_order: &[WlSurface],
    ) -> Option<(WlSurface, crate::ssd::SsdAction)> {
        let bar_h = crate::ssd::SsdDecoration::bar_height();
        for entry in self.entries_top_down(z_order) {
            if !entry.surface.is_mapped() || entry.minimized { continue; }
            if is_undecorated(&entry.surface) { continue; }
            let geo = entry.surface.geometry();
            if let Some(action) = crate::ssd::hit_test(geo, point, bar_h) {
                let wl = entry.surface.wl_surface()?;
                return Some((wl, action));
            }
            if crate::ssd::body_hit_test(geo, point, bar_h) {
                return None;
            }
        }
        None
    }

    pub fn set_minimized(&mut self, wl: &WlSurface, minimized: bool) {
        for e in &mut self.entries {
            if e.surface.wl_surface().as_ref().map_or(false, |s| s == wl) {
                e.minimized = minimized;
                let _ = e.surface.set_hidden(minimized);
                return;
            }
        }
    }

    pub fn toggle_maximize(&mut self, wl: &WlSurface, available: Rectangle<i32, Logical>) {
        for e in &mut self.entries {
            if !e.surface.wl_surface().as_ref().map_or(false, |s| s == wl) { continue; }
            if e.surface.is_maximized() {
                let restore = e.saved_geo.take().unwrap_or_else(|| e.surface.geometry());
                let _ = e.surface.set_maximized(false);
                let _ = e.surface.configure(restore);
            } else {
                e.saved_geo = Some(e.surface.geometry());
                let bar_h = crate::ssd::SsdDecoration::bar_height();
                let mut target = available;
                target.loc.y += bar_h;
                target.size.h = (target.size.h - bar_h).max(0);
                let _ = e.surface.set_maximized(true);
                let _ = e.surface.configure(target);
            }
            return;
        }
    }

    pub fn request_frames(&self, runtime: u32) {
        for entry in &self.entries {
            if !entry.surface.is_mapped() { continue; }
            let Some(wl) = entry.surface.wl_surface() else { continue };
            compositor::with_surface_tree_downward(
                &wl,
                (),
                |_, _, _| TraversalAction::DoChildren(()),
                |_, data, _| {
                    let mut attrs = data.cached_state.get::<smithay::wayland::compositor::SurfaceAttributes>();
                    for cb in attrs.current().frame_callbacks.drain(..) {
                        cb.done(runtime);
                    }
                },
                |_, _, _| true,
            );
        }
    }


    /// Walk every mapped X11Surface, pull buffers from its wl_surface tree,
    /// and import them as GL textures. Mirrors `Window::import_buffers` but
    /// stands alone because X11 has no `Window<S>` instance.
    pub fn import_buffers(&mut self, renderer: &mut GlesRenderer, scale: f64) {
        for entry in &mut self.entries {
            entry.texture_cache.clear();

            let Some(wl) = entry.surface.wl_surface() else { continue };

            compositor::with_surface_tree_upward(
                &wl,
                Point::<i32, Logical>::from((0, 0)),
                |_, surface_data, location| {
                    let Some(data) = surface_data.data_map.get::<RefCell<ForgeSurfaceData>>() else {
                        return TraversalAction::SkipChildren;
                    };
                    let mut data = data.borrow_mut();

                    let mut location = *location;
                    if surface_data.role == Some("subsurface") {
                        let mut subsurface = surface_data
                            .cached_state
                            .get::<smithay::wayland::compositor::SubsurfaceCachedState>();
                        location += subsurface.current().location;
                    }
                    data.location = location;

                    TraversalAction::DoChildren(location)
                },
                |surface, surface_data, _| {
                    let Some(data) = surface_data.data_map.get::<RefCell<ForgeSurfaceData>>() else {
                        return;
                    };
                    let mut data = data.borrow_mut();

                    if let Some(texture) = &data.texture {
                        texture.set_location(data.location);
                        entry.texture_cache.push((texture.clone(), data.location));
                        return;
                    }

                    let Some(buffer) = &data.buffer else { return };
                    let damage = data.damage.buffer();

                    let _ = scale;
                    let texture = match single_pixel_buffer::get_single_pixel_buffer(buffer) {
                        Ok(spb) => Texture::from_spb(
                            spb.rgba8888(), None, data.location, &data, surface,
                        ),
                        Err(_) => match renderer.import_buffer(buffer, Some(surface_data), damage) {
                            Some(Ok(tex)) => {
                                data.buffer = None;
                                Texture::from_surface(tex, None, data.location, &data, surface)
                            }
                            _ => {
                                tracing::error!("x11: unable to import buffer");
                                data.buffer = None;
                                return;
                            }
                        },
                    };

                    let render_texture = RenderTexture::new(texture);
                    entry.texture_cache.push((render_texture.clone(), data.location));
                    data.texture = Some(render_texture);

                    data.damage.clear();
                },
                |_, _, _| true,
            );
        }
    }

    pub fn paint_all(
        &mut self,
        out: &mut Vec<ForgeElement>,
        canvas: &crate::output::Canvas,
        renderer: &mut GlesRenderer,
        text_renderer: &mut crate::text::TextRenderer,
        pointer_pos: Option<Point<f64, Logical>>,
    ) {
        let wls: Vec<WlSurface> = self.entries.iter()
            .filter_map(|e| e.surface.wl_surface())
            .collect();
        for wl in &wls {
            self.paint_one(wl, out, canvas, renderer, text_renderer, pointer_pos);
        }
    }

    pub fn paint_remaining(
        &mut self,
        skip: &std::collections::HashSet<u32>,
        out: &mut Vec<ForgeElement>,
        canvas: &crate::output::Canvas,
        renderer: &mut GlesRenderer,
        text_renderer: &mut crate::text::TextRenderer,
        pointer_pos: Option<Point<f64, Logical>>,
    ) {
        use smithay::reexports::wayland_server::Resource as _;
        let wls: Vec<WlSurface> = self.entries.iter()
            .filter_map(|e| e.surface.wl_surface())
            .filter(|s| !skip.contains(&s.id().protocol_id()))
            .collect();
        for wl in &wls {
            self.paint_one(wl, out, canvas, renderer, text_renderer, pointer_pos);
        }
    }

    pub fn paint_one(
        &mut self,
        wl: &WlSurface,
        out: &mut Vec<ForgeElement>,
        canvas: &crate::output::Canvas,
        renderer: &mut GlesRenderer,
        text_renderer: &mut crate::text::TextRenderer,
        pointer_pos: Option<Point<f64, Logical>>,
    ) -> bool {
        let scale = canvas.scale();
        let desktop = !canvas.is_mobile();

        let Some(idx) = self.entries.iter().position(|e| {
            e.surface.wl_surface().as_ref().map_or(false, |s| s == wl)
        }) else { return false };

        let entry = &mut self.entries[idx];
        if !entry.surface.is_mapped() || entry.minimized { return false; }
        let geo = entry.surface.geometry();
        let undecorated = is_undecorated(&entry.surface);
        let title = if !undecorated {
            let t = entry.surface.title();
            Some(if t.is_empty() { entry.surface.class() } else { t })
        } else { None };

        if desktop {
            if let Some(title) = title {
                let ssd = entry.ssd.get_or_insert_with(crate::ssd::SsdDecoration::new);
                ssd.render(out, canvas, renderer, text_renderer, &title, geo, false, pointer_pos);
            }
        }

        for (texture, location) in &entry.texture_cache {
            let pos_log: Point<i32, Logical> = geo.loc + *location;
            let pos_phys: Point<i32, Physical> = pos_log.to_physical_precise_round(scale);
            let size_phys: smithay::utils::Size<i32, Physical> =
                texture.size().to_physical_precise_round(scale);
            let bounds = Rectangle::<i32, Physical>::new(pos_phys, size_phys);
            ForgeElement::add_element(out, texture.clone(), pos_phys, bounds, None, scale);
        }
        true
    }
}
