// xfwl4 -- Wayland compositor for the Xfce Desktop Environment
//
// Copyright (C) 2026 Brian Tarricone <brian@tarricone.org>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <https://www.gnu.org/licenses/>.
//
// Based on xfwm4/src/tabwin.c, which is:
//
// Copyright (C) 2002-2015 Olivier Fourdan

use anyhow::anyhow;
use glib::{ObjectExt, SignalHandlerId, StaticType, subclass::types::ObjectSubclassIsExt};
use gtk::{
    cairo,
    glib::{self, Object},
    traits::WidgetExt,
};

use crate::{
    protocols::xfwl4_compositor_ui::proto::xfwl4_ui_tabwin_v1 as tabwin_proto_server,
    ui::{
        compositor_ui_protocol::proto::xfwl4_ui_tabwin_v1::{self as tabwin_proto_client},
        util::{WidgetExtExt, style_property_value_for_type},
    },
    util::{
        cairo_ext::CairoImageSurfaceExt,
        icon::{Argb32Pixels, Icon},
    },
};

pub(super) const TABWIN_WIDGET_NAME: &str = "xfwm-tabwin";
pub(super) const TABWIN_DEFAULT_CSS: &str = r#"#xfwm-tabwin {
  padding: 4px;
  border-radius: 10px;
  border: 1px solid @theme_selected_bg_color;
  background-color: @theme_bg_color;
}"#;
pub const TABWIN_WINDOW_TITLE: &str = "Tabwin";

const WIN_ICON_SIZE: i32 = 48;
pub const WIN_PREVIEW_SIZE: i32 = 6 * WIN_ICON_SIZE;
const LISTVIEW_WIN_ICON_SIZE: i32 = WIN_ICON_SIZE / 2;
const WIN_ICON_BORDER: u32 = 5;
const WIN_MAX_RATIO: f64 = 0.8;
const LABEL_HEIGHT: u32 = 30;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, glib::Enum)]
#[enum_type(name = "TabwinMode")]
pub enum TabwinMode {
    #[default]
    Grid,
    List,
}

impl TryFrom<i32> for TabwinMode {
    type Error = anyhow::Error;

    fn try_from(value: i32) -> Result<Self, Self::Error> {
        match value {
            0 => Ok(Self::Grid),
            1 => Ok(Self::List),
            invalid => Err(anyhow!("Invalid tabwin mode '{invalid}'")),
        }
    }
}

impl From<tabwin_proto_server::TabwinMode> for TabwinMode {
    fn from(value: tabwin_proto_server::TabwinMode) -> Self {
        match value {
            tabwin_proto_server::TabwinMode::Grid => Self::Grid,
            tabwin_proto_server::TabwinMode::List => Self::List,
        }
    }
}

impl From<TabwinMode> for tabwin_proto_server::TabwinMode {
    fn from(value: TabwinMode) -> Self {
        match value {
            TabwinMode::Grid => Self::Grid,
            TabwinMode::List => Self::List,
        }
    }
}

impl From<tabwin_proto_client::TabwinMode> for TabwinMode {
    fn from(value: tabwin_proto_client::TabwinMode) -> Self {
        match value {
            tabwin_proto_client::TabwinMode::Grid => Self::Grid,
            tabwin_proto_client::TabwinMode::List => Self::List,
        }
    }
}

impl From<TabwinMode> for tabwin_proto_client::TabwinMode {
    fn from(value: TabwinMode) -> Self {
        match value {
            TabwinMode::Grid => Self::Grid,
            TabwinMode::List => Self::List,
        }
    }
}

#[derive(Debug)]
pub struct TabwinWindow {
    pub window_id: u32,
    pub app_name: Option<String>,
    pub title: String,
    pub preview_icon: Option<Argb32Pixels>,
    pub app_icon: Option<Icon>,
    pub is_minimized: bool,
}

#[derive(Debug)]
pub struct TabwinWindowUpdate {
    pub preview_icon: Option<Argb32Pixels>,
    pub app_icon: Option<Icon>,
}

struct TabwinMetrics {
    cycle_preview: bool,
    icon_size: u32,
    label_height: u32,
    grid_cols: u32,
    grid_rows: u32,
}

glib::wrapper! {
    pub struct Tabwin(ObjectSubclass<imp::Tabwin>)
        @extends gtk::Window, gtk::Container, gtk::Widget;
}

impl Tabwin {
    #[allow(clippy::too_many_arguments)]
    pub fn new(
        output_size: (i32, i32),
        output_scale: i32,
        mode: TabwinMode,
        cycle_preview: bool,
        window_opacity: f64,
        style_provider: Option<&gtk::CssProvider>,
    ) -> Self {
        let tabwin: Self = Object::builder()
            // Widget
            .property("opacity", window_opacity)
            .property("app-paintable", true)
            // Window
            .property("type", gtk::WindowType::Popup)
            .property("title", TABWIN_WINDOW_TITLE)
            .property("decorated", false)
            .property("default-width", 0)
            .property("default-height", 0)
            // Tabwin
            .property("monitor-width", output_size.0)
            .property("monitor-height", output_size.1)
            .property("monitor-scale", output_scale)
            .property("mode", mode)
            .property("cycle-preview", cycle_preview)
            .property("fallback-style-provider", style_provider.cloned())
            .build();

        tabwin
    }

    pub fn init_clients(&self, clients: Vec<TabwinWindow>, initial_selection: u32) {
        self.imp().init_clients(clients, initial_selection);
    }

    pub fn selected(&self) -> Option<u32> {
        self.imp().selected()
    }

    pub fn select_next(&self) -> Option<u32> {
        self.imp().select_next()
    }

    pub fn select_previous(&self) -> Option<u32> {
        self.imp().select_previous()
    }

    pub fn select_up(&self) -> Option<u32> {
        self.imp().select_up()
    }

    pub fn select_down(&self) -> Option<u32> {
        self.imp().select_down()
    }

    pub fn select_left(&self) -> Option<u32> {
        self.imp().select_left()
    }

    pub fn select_right(&self) -> Option<u32> {
        self.imp().select_right()
    }

    pub fn append_client(&self, _client: TabwinWindow) {
        // TODO
    }

    pub fn remove_client(&self, _window_id: u32) {
        // TODO
    }

    pub fn update_client(&self, window_id: u32, client: TabwinWindowUpdate) {
        self.imp().update_client(window_id, client);
    }

    pub fn connect_image_sizes<F>(&self, callback: F) -> SignalHandlerId
    where
        F: Fn(&Tabwin, u32, u32) + Send + Sync + 'static,
    {
        self.connect("image-sizes", false, move |args: &[glib::Value]| {
            if let Some(tabwin) = args.first().and_then(|v| v.get::<Tabwin>().ok())
                && let Some(preview_size) = args.get(1).and_then(|v| v.get::<u32>().ok())
                && let Some(icon_size) = args.get(2).and_then(|v| v.get::<u32>().ok())
            {
                callback(&tabwin, preview_size, icon_size);
            }
            None
        })
    }

    pub fn connect_selected<F>(&self, callback: F) -> SignalHandlerId
    where
        F: Fn(&Tabwin, u32) + Send + Sync + 'static,
    {
        self.connect("selected", false, move |args: &[glib::Value]| {
            if let Some(tabwin) = args.first().and_then(|v| v.get::<Tabwin>().ok())
                && let Some(selected) = args.get(1).and_then(|v| v.get::<u32>().ok())
            {
                callback(&tabwin, selected);
            }
            None
        })
    }

    pub fn connect_activated<F>(&self, callback: F) -> SignalHandlerId
    where
        F: Fn(&Tabwin, u32) + Send + Sync + 'static,
    {
        self.connect("activated", false, move |args: &[glib::Value]| {
            if let Some(tabwin) = args.first().and_then(|v| v.get::<Tabwin>().ok())
                && let Some(selected) = args.get(1).and_then(|v| v.get::<u32>().ok())
            {
                callback(&tabwin, selected);
            }
            None
        })
    }

    pub fn connect_cancelled<F>(&self, callback: F) -> SignalHandlerId
    where
        F: Fn(&Tabwin) + Send + Sync + 'static,
    {
        self.connect("cancelled", false, move |args: &[glib::Value]| {
            if let Some(tabwin) = args.first().and_then(|v| v.get::<Tabwin>().ok()) {
                callback(&tabwin);
            }
            None
        })
    }
}

mod imp {
    use std::{
        cell::{Cell, RefCell},
        ffi::CStr,
        sync::LazyLock,
    };

    use glib::{SignalFlags, subclass::Signal};
    use gtk::{
        cairo,
        ffi::GTK_STYLE_PROPERTY_BORDER_RADIUS,
        gdk::{self},
        glib::{self, prelude::*},
        prelude::WidgetExtManual,
        subclass::prelude::*,
        traits::{BoxExt, ContainerExt, GridExt, GtkWindowExt, ImageExt, LabelExt, StyleContextExt, WidgetExt},
    };
    use indexmap::IndexMap;

    use crate::{
        ui::{
            tabwin::{TabwinMetrics, TabwinWindowUpdate, calculate_tabwin_metrics, small_icon_size},
            util::WidgetClassSubclassExtExt,
        },
        util::icon::{Argb32Pixels, Icon},
    };

    use super::{
        LISTVIEW_WIN_ICON_SIZE, TABWIN_WIDGET_NAME, TabwinMode, TabwinWindow, WIN_ICON_BORDER, WIN_ICON_SIZE, WIN_PREVIEW_SIZE,
        composite_client_preview_icon,
    };

    struct IconListClient {
        app_name: Option<String>,
        title: String,
        icon: Option<gtk::cairo::Surface>,
        is_minimized: bool,
    }

    struct IconListWidget {
        app_name: Option<String>,
        title: String,
        is_minimized: bool,
        window_button: gtk::Button,
        icon_image: gtk::Image,
        label: gtk::Label,
    }

    struct IconListData {
        icons: Vec<(u32, IconListClient)>,
        icon_size: u32,
        label_height: u32,
        grid_cols: u32,
        grid_rows: u32,
    }

    #[derive(Default, glib::Properties)]
    #[properties(wrapper_type = super::Tabwin)]
    pub struct Tabwin {
        #[property(name = "fallback-style-provider", construct_only, get)]
        fallback_style_provider: RefCell<Option<gtk::CssProvider>>,
        #[property(name = "monitor-width", construct_only, get)]
        monitor_width: Cell<i32>,
        #[property(name = "monitor-height", construct_only, get)]
        monitor_height: Cell<i32>,
        #[property(name = "monitor-scale", construct_only, get)]
        monitor_scale: Cell<i32>,
        #[property(name = "mode", construct_only, get, builder(TabwinMode::default()))]
        mode: Cell<TabwinMode>,
        #[property(name = "cycle-preview", construct_only, get, default = true)]
        cycle_preview: Cell<bool>,

        client_widgets: RefCell<IndexMap<u32, IconListWidget>>,
        selected_window_id: RefCell<Option<u32>>,

        icon_size: Cell<u32>,
        grid_cols: Cell<u32>,
        grid_rows: Cell<u32>,

        top_vbox: RefCell<gtk::Box>,
        label: RefCell<gtk::Label>,
        container: RefCell<gtk::Grid>,
        size: RefCell<(u32, u32)>,
    }

    #[glib::object_subclass]
    impl ObjectSubclass for Tabwin {
        const NAME: &'static str = "Xfwl4Tabwin";
        type Type = super::Tabwin;
        type ParentType = gtk::Window;

        fn class_init(klass: &mut Self::Class) {
            klass.install_style_property_from_pspec(
                glib::ParamSpecInt::builder("preview-size")
                    .nick("preview size")
                    .blurb("size of the app preview")
                    .minimum(1)
                    .default_value(WIN_PREVIEW_SIZE)
                    .read_only()
                    .build(),
            );

            klass.install_style_property_from_pspec(
                glib::ParamSpecInt::builder("icon-size")
                    .nick("icon size")
                    .blurb("size of the app icon")
                    .minimum(1)
                    .default_value(WIN_ICON_SIZE)
                    .read_only()
                    .build(),
            );

            klass.install_style_property_from_pspec(
                glib::ParamSpecInt::builder("listview-icon-size")
                    .nick("listview icon size")
                    .blurb("ize of the app icon in listview mode")
                    .minimum(1)
                    .default_value(LISTVIEW_WIN_ICON_SIZE)
                    .read_only()
                    .build(),
            );
        }
    }

    #[glib::derived_properties]
    impl ObjectImpl for Tabwin {
        fn signals() -> &'static [Signal] {
            static SIGNALS: LazyLock<Vec<Signal>> = LazyLock::new(|| {
                vec![
                    Signal::builder("image-sizes")
                        .flags(SignalFlags::RUN_LAST)
                        .param_types([u32::static_type(), u32::static_type()])
                        .build(),
                    Signal::builder("selected")
                        .flags(SignalFlags::RUN_LAST)
                        .param_types([u32::static_type()])
                        .build(),
                    Signal::builder("activated")
                        .flags(SignalFlags::RUN_LAST)
                        .param_types([u32::static_type()])
                        .build(),
                    Signal::builder("cancelled").flags(SignalFlags::RUN_LAST).build(),
                ]
            });
            &SIGNALS
        }

        fn constructed(&self) {
            self.parent_constructed();

            let instance = self.obj();
            instance.set_position(gtk::WindowPosition::None);
            instance.set_widget_name(TABWIN_WIDGET_NAME);

            if let Some(fallback_style_provider) = self.fallback_style_provider.borrow().as_ref() {
                instance
                    .style_context()
                    .add_provider(fallback_style_provider, gtk::STYLE_PROVIDER_PRIORITY_APPLICATION);
            }

            instance.realize();

            let ctx = instance.style_context();

            let border_radius_prop = CStr::from_bytes_with_nul(GTK_STYLE_PROPERTY_BORDER_RADIUS)
                .expect("strings from gtk should be valid")
                .to_string_lossy();
            let border_radius = ctx
                .style_property_for_state(&border_radius_prop, gtk::StateFlags::NORMAL)
                .get::<i32>()
                .unwrap_or(0);
            let border = ctx.border(gtk::StateFlags::NORMAL);
            let padding = ctx.padding(gtk::StateFlags::NORMAL);
            let border_width = (border_radius as u32)
                + border.left.max(border.right.max(border.top.max(border.bottom))) as u32
                + padding.left.max(padding.right.max(padding.top.max(padding.bottom))) as u32;
            instance.set_border_width(border_width);

            let vbox = gtk::Box::new(gtk::Orientation::Vertical, 3);
            instance.add(&vbox);

            let css_class = match self.mode.get() {
                TabwinMode::Grid => {
                    let label = gtk::Label::new(Some(""));
                    label.set_justify(gtk::Justification::Center);
                    label.set_ellipsize(gtk::pango::EllipsizeMode::End);
                    vbox.pack_end(&label, true, true, 0);
                    self.label.replace(label);

                    "tabwin-app-grid"
                }
                TabwinMode::List => "tabwin-app-list",
            };
            ctx.add_class(css_class);

            self.top_vbox.replace(vbox);

            instance.connect_configure_event(|tabwin, event| tabwin.imp().handle_configure(event));
            instance.connect_draw(|tabwin, ctx| tabwin.imp().handle_draw(ctx));
        }
    }

    impl WidgetImpl for Tabwin {}

    impl ContainerImpl for Tabwin {}

    impl BinImpl for Tabwin {}

    impl WindowImpl for Tabwin {}

    impl Tabwin {
        pub(super) fn init_clients(&self, clients: Vec<TabwinWindow>, initial_selection: u32) {
            let (window_list, button_widgets) = self.create_window_list(clients);
            self.top_vbox.borrow().pack_start(&window_list, true, true, 0);
            self.container.replace(window_list);
            self.client_widgets.replace(button_widgets);

            self.set_selected(initial_selection);
        }

        fn create_window_list(&self, clients: Vec<TabwinWindow>) -> (gtk::Grid, IndexMap<u32, IconListWidget>) {
            let instance = self.obj();
            instance.realize();

            let grid = gtk::Grid::builder()
                .row_homogeneous(true)
                .row_spacing(4)
                .column_homogeneous(true)
                .column_spacing(4)
                .build();

            let icon_list_data = self.build_icon_list(clients);
            self.grid_cols.replace(icon_list_data.grid_cols);
            self.grid_rows.replace(icon_list_data.grid_rows);

            let size_request = icon_list_data.icon_size + icon_list_data.label_height + 2 * WIN_ICON_BORDER;

            let mut pack_pos: u32 = 0;
            #[allow(clippy::mutable_key_type)]
            let widgets = icon_list_data
                .icons
                .into_iter()
                .map(|(window_id, icon_list_client)| {
                    let IconListClient {
                        app_name,
                        title,
                        icon,
                        is_minimized,
                    } = icon_list_client;

                    let window_button = gtk::Button::builder().relief(gtk::ReliefStyle::None).build();
                    // Make it so the toplevel window gets all key events.
                    window_button.set_events(window_button.events() & !(gdk::EventMask::KEY_PRESS_MASK | gdk::EventMask::KEY_RELEASE_MASK));

                    // We have to use 'button-press-event' here, because 'activate' will not
                    // fire if there are modifier keys held, which will often be the case with
                    // the tabwin.
                    window_button.connect_button_press_event(move |button, _| {
                        if let Some(window) = button.toplevel().and_then(|toplevel| toplevel.downcast::<super::Tabwin>().ok()) {
                            window.emit_by_name::<()>("activated", &[&window_id]);
                            window.close();
                            glib::Propagation::Stop
                        } else {
                            glib::Propagation::Proceed
                        }
                    });

                    if self.mode.get() == TabwinMode::Grid {
                        window_button.connect_enter_notify_event({
                            let myself = instance.to_owned();
                            move |_button, _event| {
                                let imp = myself.imp();

                                if let Some(widget_data) = imp.client_widgets.borrow().get(&window_id) {
                                    widget_data.label.set_label(widget_data.app_name.as_deref().unwrap_or("..."));
                                    myself.imp().label.borrow().set_label(&widget_data.title);
                                }
                                glib::Propagation::Proceed
                            }
                        });

                        window_button.connect_leave_notify_event({
                            let myself = instance.to_owned();
                            move |_button, _event| {
                                let imp = myself.imp();

                                if let Some(widget_data) = imp.client_widgets.borrow().get(&window_id) {
                                    widget_data.label.set_label("");
                                }

                                if let Some(selected) = imp.selected_window_id.borrow().as_ref()
                                    && let Some(widget_data) = imp.client_widgets.borrow().get(selected)
                                {
                                    myself.imp().label.borrow().set_label(&widget_data.title);
                                }

                                glib::Propagation::Proceed
                            }
                        });
                    }

                    let mut icon_image = gtk::Image::builder();
                    if let Some(icon) = icon {
                        icon_image = icon_image.surface(&icon);
                    }
                    let icon_image = icon_image.build();

                    let (button_box, button_label, left_attach, top_attach) = match self.mode.get() {
                        TabwinMode::Grid => {
                            window_button.set_size_request(size_request as i32, size_request as i32);

                            let button_box = gtk::Box::builder().orientation(gtk::Orientation::Vertical).spacing(0).build();
                            let button_label = gtk::Label::builder().label("").xalign(0.5).yalign(1.).build();

                            icon_image.set_halign(gtk::Align::Center);
                            icon_image.set_valign(gtk::Align::End);

                            button_box.pack_start(&icon_image, true, true, 0);

                            (
                                button_box,
                                button_label,
                                pack_pos % icon_list_data.grid_cols,
                                pack_pos / icon_list_data.grid_cols,
                            )
                        }

                        TabwinMode::List => {
                            let label_width = self.monitor_width.get() as f64 / (icon_list_data.grid_cols as f64 + 1.);

                            window_button.set_size_request(
                                label_width as i32,
                                if icon_list_data.icon_size < icon_list_data.label_height {
                                    icon_list_data.label_height as i32 + 8
                                } else {
                                    icon_list_data.icon_size as i32 + 8
                                },
                            );

                            let button_box = gtk::Box::builder().orientation(gtk::Orientation::Horizontal).spacing(6).build();
                            let button_label = gtk::Label::builder().label(&*title).xalign(0.).yalign(0.5).build();

                            icon_image.set_halign(gtk::Align::Center);
                            icon_image.set_valign(gtk::Align::Center);

                            button_box.pack_start(&icon_image, false, false, 0);

                            (
                                button_box,
                                button_label,
                                pack_pos / icon_list_data.grid_rows,
                                pack_pos % icon_list_data.grid_rows,
                            )
                        }
                    };

                    window_button.add(&button_box);

                    button_label.set_justify(gtk::Justification::Center);
                    button_label.set_ellipsize(gtk::pango::EllipsizeMode::End);
                    button_box.pack_start(&button_label, true, true, 0);

                    grid.attach(&window_button, left_attach as i32, top_attach as i32, 1, 1);

                    pack_pos += 1;

                    (
                        window_id,
                        IconListWidget {
                            app_name,
                            title,
                            is_minimized,
                            window_button,
                            icon_image,
                            label: button_label,
                        },
                    )
                })
                .collect();

            (grid, widgets)
        }

        fn icon_for_client(
            &self,
            icon_theme: &gtk::IconTheme,
            preview: Option<Argb32Pixels>,
            app_icon: Option<Icon>,
            icon_size: u32,
            scale: i32,
            is_minimized: bool,
        ) -> anyhow::Result<cairo::Surface> {
            if self.mode.get() == TabwinMode::Grid && self.cycle_preview.get() {
                composite_client_preview_icon(icon_theme, preview, app_icon, icon_size, icon_size, scale, is_minimized)
            } else {
                app_icon
                    .map(|icon| {
                        icon.load(icon_size, icon_size, scale as f64, icon_theme)
                            .or_else(|_| Icon::fallback().load(icon_size, icon_size, scale as f64, icon_theme))
                    })
                    .unwrap_or_else(|| {
                        let scaled_size = (icon_size * scale as u32) as i32;
                        let surface = cairo::ImageSurface::create(cairo::Format::ARgb32, scaled_size, scaled_size)?;
                        surface.set_device_scale(scale as f64, scale as f64);
                        let cr = cairo::Context::new(&surface)?;
                        cr.set_operator(cairo::Operator::Source);
                        cr.set_source_rgba(0.13, 0.13, 0.13, 1.);
                        cr.paint()?;
                        Ok(surface)
                    })
                    .map(|surface| (*surface).clone())
            }
        }

        fn build_icon_list(&self, clients: Vec<TabwinWindow>) -> IconListData {
            let instance = self.obj();

            let scale = self.monitor_scale.get();
            let n_clients = clients.len();
            let TabwinMetrics {
                cycle_preview,
                icon_size,
                label_height,
                grid_cols,
                grid_rows,
            } = calculate_tabwin_metrics(
                self.mode.get(),
                n_clients,
                self.cycle_preview.get(),
                self.monitor_width.get(),
                self.monitor_height.get(),
                Some(&instance),
            );
            self.cycle_preview.set(cycle_preview);
            self.icon_size.set(icon_size);

            let (window_preview_size, window_icon_size) = if self.obj().mode() == TabwinMode::Grid && cycle_preview {
                (icon_size, small_icon_size(icon_size, scale as u32))
            } else {
                (0, icon_size)
            };
            self.obj()
                .emit_by_name::<()>("image-sizes", &[&window_preview_size, &window_icon_size]);

            let icon_theme = gtk::IconTheme::default().expect("failed to get default icon theme");
            let icons = clients
                .into_iter()
                .map(|client| {
                    let icon = self
                        .icon_for_client(
                            &icon_theme,
                            client.preview_icon,
                            client.app_icon,
                            icon_size,
                            scale,
                            client.is_minimized,
                        )
                        .inspect(|err| tracing::warn!("Failed to render window icon: {err}"))
                        .ok();

                    (
                        client.window_id,
                        IconListClient {
                            app_name: client.app_name,
                            title: client.title,
                            icon,
                            is_minimized: client.is_minimized,
                        },
                    )
                })
                .collect();

            IconListData {
                icons,
                icon_size,
                label_height,
                grid_cols,
                grid_rows,
            }
        }

        fn set_selected(&self, selected: u32) {
            let update_labels = self.mode.get() == TabwinMode::Grid;

            if let Some(prev_selected) = self.selected_window_id.borrow_mut().take()
                && let Some(prev_widget_data) = self.client_widgets.borrow().get(&prev_selected)
            {
                if update_labels {
                    prev_widget_data.label.set_label("");
                }
                prev_widget_data.window_button.unset_state_flags(gtk::StateFlags::CHECKED);
            }

            if let Some(widget_data) = self.client_widgets.borrow().get(&selected) {
                widget_data.window_button.set_state_flags(gtk::StateFlags::CHECKED, false);
                if update_labels {
                    widget_data.label.set_label(widget_data.app_name.as_deref().unwrap_or("..."));
                    self.label.borrow().set_label(&widget_data.title);
                }
                widget_data.window_button.grab_focus();
            } else if update_labels {
                self.label.borrow().set_label("");
            }

            self.selected_window_id.replace(Some(selected));

            self.obj().emit_by_name::<()>("selected", &[&selected]);
        }

        pub(super) fn selected(&self) -> Option<u32> {
            *self.selected_window_id.borrow()
        }

        pub(super) fn select_next(&self) -> Option<u32> {
            self.select_sequential(|index, n_clients| (index + 1) % n_clients)
        }

        pub(super) fn select_previous(&self) -> Option<u32> {
            self.select_sequential(|index, n_clients| if index == 0 { n_clients - 1 } else { index - 1 })
        }

        pub(super) fn select_up(&self) -> Option<u32> {
            let dec = |i: u32, total: u32| if i == 0 { total - 1 } else { i - 1 };
            match self.mode.get() {
                TabwinMode::Grid => self.select_perpendicular(dec),
                TabwinMode::List => self.select_sequential(dec),
            }
        }

        pub(super) fn select_down(&self) -> Option<u32> {
            let inc = |i: u32, total: u32| (i + 1) % total;
            match self.mode.get() {
                TabwinMode::Grid => self.select_perpendicular(inc),
                TabwinMode::List => self.select_sequential(inc),
            }
        }

        pub(super) fn select_left(&self) -> Option<u32> {
            let dec = |i: u32, total: u32| if i == 0 { total - 1 } else { i - 1 };
            match self.mode.get() {
                TabwinMode::Grid => self.select_sequential(dec),
                TabwinMode::List => self.select_perpendicular(dec),
            }
        }

        pub(super) fn select_right(&self) -> Option<u32> {
            let inc = |i: u32, total: u32| (i + 1) % total;
            match self.mode.get() {
                TabwinMode::Grid => self.select_sequential(inc),
                TabwinMode::List => self.select_perpendicular(inc),
            }
        }

        fn select_sequential(&self, advance: impl Fn(u32, u32) -> u32) -> Option<u32> {
            let cur_index = self
                .selected_window_id
                .borrow()
                .and_then(|window_id| self.client_widgets.borrow().get_index_of(&window_id));
            if let Some(cur_index) = cur_index {
                let new_index = advance(cur_index as u32, self.client_widgets.borrow().len() as u32);
                if let Some((window_id, _)) = self.client_widgets.borrow().get_index(new_index as usize) {
                    self.set_selected(*window_id);
                    Some(*window_id)
                } else {
                    None
                }
            } else {
                None
            }
        }

        fn select_perpendicular(&self, advance: impl Fn(u32, u32) -> u32) -> Option<u32> {
            let cur_index = self
                .selected_window_id
                .borrow()
                .and_then(|window_id| self.client_widgets.borrow().get_index_of(&window_id));
            if let Some(cur_index) = cur_index {
                let cur_index = cur_index as u32;
                let new_index = {
                    let n_clients = self.client_widgets.borrow().len() as u32;
                    let (primary, secondary) = match self.mode.get() {
                        TabwinMode::Grid => (self.grid_cols.get(), self.grid_rows.get()),
                        TabwinMode::List => (self.grid_rows.get(), self.grid_cols.get()),
                    };
                    let total = primary * secondary;

                    let perp_index = (cur_index % primary) * secondary + (cur_index / primary);
                    let mut new_perp = perp_index;
                    loop {
                        new_perp = advance(new_perp, total);
                        let new_index = (new_perp % secondary) * primary + (new_perp / secondary);
                        if new_index < n_clients {
                            break new_index;
                        }
                    }
                };

                if let Some((window_id, _)) = self.client_widgets.borrow().get_index(new_index as usize) {
                    self.set_selected(*window_id);
                    Some(*window_id)
                } else {
                    None
                }
            } else {
                None
            }
        }

        pub(super) fn update_client(&self, window_id: u32, update: TabwinWindowUpdate) {
            if let Some(widget) = self.client_widgets.borrow().get(&window_id)
                && update.app_icon.is_some()
            {
                let icon_theme = gtk::IconTheme::default().expect("failed to get default icon theme");
                let scale = self.monitor_scale.get();
                match self.icon_for_client(
                    &icon_theme,
                    update.preview_icon,
                    update.app_icon,
                    self.icon_size.get(),
                    scale,
                    widget.is_minimized,
                ) {
                    Ok(icon) => widget.icon_image.set_surface(Some(&icon)),
                    Err(err) => tracing::warn!("Failed to render window icon: {err}"),
                }
            }
        }

        fn handle_configure(&self, event: &gtk::gdk::EventConfigure) -> bool {
            self.size.replace(event.size());
            false
        }

        fn handle_draw(&self, cr: &gtk::cairo::Context) -> glib::Propagation {
            let instance = self.obj();

            let allocation = instance.allocation();
            let ctx = instance.style_context();

            gtk::render_background(&ctx, cr, 0., 0., allocation.width() as f64, allocation.height() as f64);
            gtk::render_frame(&ctx, cr, 0., 0., allocation.width() as f64, allocation.height() as f64);

            glib::Propagation::Proceed
        }
    }
}

fn small_icon_size(full_icon_size: u32, scale: u32) -> u32 {
    (full_icon_size / 4).min(48 / scale)
}

fn composite_client_preview_icon(
    icon_theme: &gtk::IconTheme,
    preview_icon: Option<Argb32Pixels>,
    app_icon: Option<Icon>,
    width: u32,
    height: u32,
    scale: i32,
    is_minimized: bool,
) -> anyhow::Result<cairo::Surface> {
    let phys_width = width * scale as u32;
    let phys_height = height * scale as u32;
    let mut icon_surface = cairo::ImageSurface::create(cairo::Format::ARgb32, phys_width as i32, phys_height as i32)?;
    icon_surface.set_device_scale(scale as f64, scale as f64);

    let cr = cairo::Context::new(&icon_surface)?;

    if let Some(app_content) = preview_icon
        .and_then(|pixels| {
            pixels
                .into_surface()
                .and_then(|surface| {
                    surface.set_device_scale(scale as f64, scale as f64);
                    surface.scale_aspect(phys_width, phys_height)
                })
                .ok()
        })
        .or_else(|| Icon::fallback().load(width, height, scale as f64, icon_theme).ok())
    {
        let preview_width = app_content.width() as f64 / scale as f64;
        let preview_height = app_content.height() as f64 / scale as f64;
        let dest_x = (width as f64 - preview_width) / 2.;
        let dest_y = (height as f64 - preview_height) / 2.;

        cr.save()?;
        cr.set_operator(cairo::Operator::Source);
        cr.set_source_surface(&app_content, dest_x, dest_y)?;
        cr.paint()?;
        cr.restore()?;
    }

    let small_icon_size = small_icon_size(width.min(height), scale as u32);
    if let Some(small_icon) = app_icon
        .and_then(|icon| icon.load(small_icon_size, small_icon_size, scale as f64, icon_theme).ok())
        .or_else(|| {
            Icon::fallback()
                .load(small_icon_size, small_icon_size, scale as f64, icon_theme)
                .ok()
        })
    {
        let icon_width = small_icon.width() as f64 / scale as f64;
        let icon_height = small_icon.height() as f64 / scale as f64;
        let dest_x = (width as f64 - icon_width) / 2.;
        let dest_y = height as f64 - icon_height;

        cr.save()?;
        cr.set_operator(cairo::Operator::Over);
        cr.set_source_surface(&small_icon, dest_x, dest_y)?;
        cr.paint()?;
        cr.restore()?;
    }

    drop(cr);

    if is_minimized {
        icon_surface.saturate_and_pixelate_in_place(0.55);
    }

    Ok((*icon_surface).clone())
}

fn calculate_tabwin_metrics(
    mode: TabwinMode,
    n_clients: usize,
    mut cycle_preview: bool,
    monitor_width: i32,
    monitor_height: i32,
    tabwin: Option<&Tabwin>,
) -> TabwinMetrics {
    let label_height = tabwin
        .map(|tabwin| {
            let layout = tabwin.create_pango_layout(Some(""));
            layout.pixel_size().1
        })
        .filter(|height| *height > 0)
        .unwrap_or(LABEL_HEIGHT as i32) as u32;

    match mode {
        TabwinMode::Grid => {
            let calc_grid_size = move |size_request: u32| {
                let grid_cols = (monitor_width as f64 * WIN_MAX_RATIO / size_request as f64).floor() as u32;
                let grid_rows = (n_clients as f64 / grid_cols as f64).ceil() as u32;
                (grid_cols, grid_rows)
            };

            let standard_icon_size = tabwin
                .map(|tabwin| tabwin.style_property::<i32>("icon-size"))
                .or_else(|| style_property_value_for_type(Tabwin::static_type(), "icon_size"))
                .unwrap_or(WIN_ICON_SIZE) as u32;

            let mut icon_size = if cycle_preview {
                tabwin
                    .map(|tabwin| tabwin.style_property::<i32>("preview-size"))
                    .or_else(|| style_property_value_for_type(Tabwin::static_type(), "preview-size"))
                    .unwrap_or(WIN_PREVIEW_SIZE) as u32
            } else {
                standard_icon_size
            };

            let mut size_request = icon_size + label_height + 2 * WIN_ICON_BORDER;
            let (mut grid_cols, mut grid_rows) = calc_grid_size(size_request);

            // Halve the icon size until everything fits
            while (size_request * grid_rows + label_height) as f64 > monitor_height as f64 * WIN_MAX_RATIO {
                icon_size /= 2;
                if cycle_preview && icon_size <= standard_icon_size {
                    cycle_preview = false;
                    icon_size = standard_icon_size;
                }

                size_request = icon_size + label_height + 2 * WIN_ICON_BORDER;
                (grid_cols, grid_rows) = calc_grid_size(size_request);

                // Don't let the icons get too small
                if icon_size < 8 {
                    icon_size = 8;
                    break;
                }
            }

            TabwinMetrics {
                cycle_preview,
                icon_size,
                label_height,
                grid_cols,
                grid_rows,
            }
        }

        TabwinMode::List => {
            let icon_size = tabwin
                .map(|tabwin| tabwin.style_property::<i32>("listview-icon-size"))
                .or_else(|| style_property_value_for_type(Tabwin::static_type(), "listview-icon-size"))
                .unwrap_or(LISTVIEW_WIN_ICON_SIZE) as u32;
            let grid_rows = ((monitor_height as f64 * WIN_MAX_RATIO) / (icon_size + 2 * WIN_ICON_BORDER) as f64).floor() as u32;
            let grid_cols = (n_clients as f64 / grid_rows as f64).ceil() as u32;

            TabwinMetrics {
                cycle_preview: false,
                icon_size,
                label_height,
                grid_cols,
                grid_rows,
            }
        }
    }
}

#[cfg(test)]
mod test {
    use super::{TABWIN_DEFAULT_CSS, TABWIN_WIDGET_NAME};

    #[test]
    pub fn ensure_tabwin_css_name_correct() {
        let leading = format!("#{TABWIN_WIDGET_NAME}");
        assert!(TABWIN_DEFAULT_CSS.starts_with(&leading));
    }
}
