use super::ImageIcon;
use crate::MAX_LABEL_WIDTH;
use crate::MONITOR_INDEX;
use crate::config::DisplayFormat;
use crate::config::DisplayFormat::*;
use crate::config::WorkspacesDisplayFormat;
use crate::render::RenderConfig;
use crate::selected_frame::SelectableFrame;
use crate::ui::CustomUi;
use crate::widgets::komorebi_layout::KomorebiLayout;
use crate::widgets::widget::BarWidget;
use eframe::egui::Align;
use eframe::egui::Color32;
use eframe::egui::Context;
use eframe::egui::CornerRadius;
use eframe::egui::FontId;
use eframe::egui::Frame;
use eframe::egui::Image;
use eframe::egui::Label;
use eframe::egui::Margin;
use eframe::egui::Response;
use eframe::egui::RichText;
use eframe::egui::Sense;
use eframe::egui::Stroke;
use eframe::egui::StrokeKind;
use eframe::egui::TextFormat;
use eframe::egui::Ui;
use eframe::egui::Vec2;
use eframe::egui::text::LayoutJob;
use eframe::egui::vec2;
use komorebi_client::Container;
use komorebi_client::PathExt;
use komorebi_client::Rect;
use komorebi_client::SocketMessage;
use komorebi_client::SocketMessage::*;
use komorebi_client::State;
use komorebi_client::Window;
use komorebi_client::Workspace;
use komorebi_client::WorkspaceLayer;
use serde::Deserialize;
use serde::Serialize;
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::collections::HashMap;
use std::io::Result as IoResult;
use std::path::Path;
use std::rc::Rc;
use std::sync::atomic::Ordering;

#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration
pub struct KomorebiConfig {
    /// Configure the Workspaces widget
    pub workspaces: Option<KomorebiWorkspacesConfig>,
    /// Configure the Layout widget
    pub layout: Option<KomorebiLayoutConfig>,
    /// Configure the Workspace Layer widget
    pub workspace_layer: Option<KomorebiWorkspaceLayerConfig>,
    /// Configure the Focused Container widget
    #[serde(alias = "focused_window")]
    pub focused_container: Option<KomorebiFocusedContainerConfig>,
    /// Configure the Locked Container widget
    pub locked_container: Option<KomorebiLockedContainerConfig>,
    /// Configure the Configuration Switcher widget
    pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspaces configuration
pub struct KomorebiWorkspacesConfig {
    /// Enable the Komorebi Workspaces widget
    pub enable: bool,
    /// Hide workspaces without any windows
    pub hide_empty_workspaces: bool,
    /// Display format of the workspace
    pub display: Option<WorkspacesDisplayFormat>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget layout configuration
pub struct KomorebiLayoutConfig {
    /// Enable the Komorebi Layout widget
    pub enable: bool,
    /// List of layout options
    pub options: Option<Vec<KomorebiLayout>>,
    /// Display format of the current layout
    pub display: Option<DisplayFormat>,
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget workspace layer configuration
pub struct KomorebiWorkspaceLayerConfig {
    /// Enable the Komorebi Workspace Layer widget
    pub enable: bool,
    /// Display format of the current layer
    pub display: Option<DisplayFormat>,
    /// Show the widget event if the layer is Tiling
    pub show_when_tiling: Option<bool>,
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget focused container configuration
pub struct KomorebiFocusedContainerConfig {
    /// Enable the Komorebi Focused Container widget
    pub enable: bool,
    /// DEPRECATED: use `display` instead (Show the icon of the currently focused container)
    #[deprecated(note = "Use `display` instead")]
    pub show_icon: Option<bool>,
    /// Display format of the currently focused container
    pub display: Option<DisplayFormat>,
}

#[derive(Copy, Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget locked container configuration
pub struct KomorebiLockedContainerConfig {
    /// Enable the Komorebi Locked Container widget
    pub enable: bool,
    /// Display format of the current locked state
    pub display: Option<DisplayFormat>,
    /// Show the widget event if the layer is unlocked
    pub show_when_unlocked: Option<bool>,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
/// Komorebi widget configuration switcher configuration
pub struct KomorebiConfigurationSwitcherConfig {
    /// Enable the Komorebi Configurations widget
    pub enable: bool,
    /// A map of display friendly name => path to configuration.json
    pub configurations: BTreeMap<String, String>,
}

impl From<&KomorebiConfig> for Komorebi {
    fn from(cfg: &KomorebiConfig) -> Self {
        let configuration_switcher = cfg.configuration_switcher.clone().map(|mut cs| {
            for location in cs.configurations.values_mut() {
                let path = Path::new(location).replace_env();
                *location = dunce::simplified(&path).to_string_lossy().to_string();
            }
            cs
        });

        Self {
            monitor_info: Rc::new(RefCell::new(MonitorInfo {
                hide_empty_workspaces: cfg
                    .workspaces
                    .map(|w| w.hide_empty_workspaces)
                    .unwrap_or_default(),
                ..Default::default()
            })),
            workspaces: cfg.workspaces.and_then(WorkspacesBar::try_from),
            layout: cfg.layout.clone(),
            focused_container: cfg
                .focused_container
                .and_then(FocusedContainerBar::try_from),
            workspace_layer: cfg.workspace_layer.and_then(WorkspaceLayerBar::try_from),
            locked_container: cfg.locked_container.and_then(LockedContainerBar::try_from),
            configuration_switcher,
        }
    }
}

#[derive(Clone, Debug)]
pub struct Komorebi {
    pub monitor_info: Rc<RefCell<MonitorInfo>>,
    pub workspaces: Option<WorkspacesBar>,
    pub layout: Option<KomorebiLayoutConfig>,
    pub focused_container: Option<FocusedContainerBar>,
    pub workspace_layer: Option<WorkspaceLayerBar>,
    pub locked_container: Option<LockedContainerBar>,
    pub configuration_switcher: Option<KomorebiConfigurationSwitcherConfig>,
}

impl BarWidget for Komorebi {
    fn render(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
        self.render_workspaces(ctx, ui, config);
        self.render_workspace_layer(ctx, ui, config);
        self.render_layout(ctx, ui, config);
        self.render_config_switcher(ui, config);
        self.render_locked_container(ctx, ui, config);
        self.render_focused_container(ctx, ui, config);
    }
}

impl Komorebi {
    /// Renders the workspace bar for the current monitor.
    /// Updates the focused workspace when a workspace is clicked.
    fn render_workspaces(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
        let monitor_info = &mut *self.monitor_info.borrow_mut();

        let bar = match &mut self.workspaces {
            Some(wg) if !monitor_info.workspaces.is_empty() => wg,
            _ => return,
        };

        bar.text_size = Vec2::splat(config.text_font_id.size);
        bar.icon_size = Vec2::splat(config.icon_font_id.size);

        config.apply_on_widget(false, ui, |ui| {
            for (index, workspace) in monitor_info.workspaces.iter().enumerate() {
                if !workspace.should_show {
                    continue;
                }

                let response = SelectableFrame::new(workspace.is_selected)
                    .show(ui, |ui| (bar.renderer)(bar, ctx, ui, workspace));

                if response.clicked() {
                    let message = FocusMonitorWorkspaceNumber(monitor_info.monitor_index, index);
                    if Self::send_with_mouse_follow_off(monitor_info, message).is_ok() {
                        monitor_info.focused_workspace_idx = Some(index);
                    }
                }
            }
        });
    }

    /// Renders the workspace layer bar for the current monitor.
    /// Handles user clicks to toggle the workspace layer.
    fn render_workspace_layer(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
        let Some(bar) = &self.workspace_layer else {
            return;
        };

        let monitor_info = &*self.monitor_info.borrow();
        let Some(layer) = monitor_info.focused_workspace_layer() else {
            return;
        };

        if (matches!(layer, WorkspaceLayer::Floating)
            || bar.show_when_tiling && matches!(layer, WorkspaceLayer::Tiling))
        {
            let size = Vec2::splat(config.icon_font_id.size);
            config.apply_on_widget(false, ui, |ui| {
                let layer_frame = SelectableFrame::new(false)
                    .show(ui, |ui| (bar.renderer)(ctx, ui, &layer, size))
                    .on_hover_text(layer.to_string());

                if layer_frame.clicked() {
                    let _ = Self::send_messages(&[
                        FocusMonitorAtCursor,
                        MouseFollowsFocus(false),
                        ToggleWorkspaceLayer,
                        MouseFollowsFocus(monitor_info.mouse_follows_focus),
                    ]);
                }
            });
        }
    }

    /// Renders the configuration switcher bar.
    /// For each available configuration, displays a selectable label.
    /// On click, sends a message to replace the current Komorebi configuration.
    fn render_config_switcher(&mut self, ui: &mut Ui, config: &mut RenderConfig) {
        let configuration_switcher = match &self.configuration_switcher {
            Some(config) if config.enable => config,
            _ => return,
        };
        for (name, location) in configuration_switcher.configurations.iter() {
            let path = Path::new(location);
            if path.is_file() {
                config.apply_on_widget(false, ui, |ui| {
                    let response = SelectableFrame::new(false)
                        .show(ui, |ui| ui.add(Label::new(name).selectable(false)));
                    if response.clicked() {
                        let path = dunce::canonicalize(path).unwrap_or_else(|_| path.to_owned());
                        let _ = Self::send_messages(&[ReplaceConfiguration(path)]);
                    }
                });
            }
        }
    }

    fn render_layout(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
        if let Some(layout_config) = &self.layout
            && layout_config.enable
        {
            let monitor_info = &mut *self.monitor_info.borrow_mut();
            let workspace_idx = monitor_info.focused_workspace_idx;
            monitor_info
                .layout
                .show(ctx, ui, config, layout_config, workspace_idx);
        }
    }

    /// Renders the locked container bar for the current monitor.
    ///
    /// Shows lock status and allows toggling the lock state when clicked.
    /// Only renders if a focused container exists and either the container is locked
    /// or `show_when_unlocked` is enabled in the bar configuration.
    fn render_locked_container(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
        let Some(bar) = &self.locked_container else {
            return;
        };

        let monitor_info = &*self.monitor_info.borrow();
        let is_locked = monitor_info
            .focused_container()
            .map(|container| container.is_locked)
            .unwrap_or_default();

        let (icon_font, txt_font) = (config.icon_font_id.clone(), config.text_font_id.clone());
        if (bar.show_when_unlocked || is_locked) && monitor_info.focused_container().is_some() {
            config.apply_on_widget(false, ui, |ui| {
                SelectableFrame::new(false)
                    .show(ui, |ui| {
                        (bar.renderer)(ctx, ui, is_locked, icon_font, txt_font)
                    })
                    .clicked()
                    .then(|| Self::send_messages(&[FocusMonitorAtCursor, ToggleLock]));
            });
        }
    }

    fn render_focused_container(&mut self, ctx: &Context, ui: &mut Ui, config: &mut RenderConfig) {
        let Some(bar) = &self.focused_container else {
            return;
        };
        let monitor_info = &*self.monitor_info.borrow();
        let Some(container) = monitor_info.focused_container() else {
            return;
        };
        config.apply_on_widget(false, ui, |ui| {
            let (len, focused_idx) = (container.windows.len(), container.focused_window_idx);

            for (idx, window) in container.windows.iter().enumerate() {
                let selected = idx == focused_idx && len != 1;
                let text_color = if selected {
                    ctx.style().visuals.selection.stroke.color
                } else {
                    ui.style().visuals.text_color()
                };

                let response = SelectableFrame::new(selected).show(ui, |ui| {
                    (bar.renderer)(bar, ctx, ui, window, text_color, idx == focused_idx)
                });

                if response.clicked() && !selected {
                    let _ = Self::send_with_mouse_follow_off(monitor_info, FocusStackWindow(idx));
                }
            }
        });
    }

    /// Sends a message to Komorebi, temporarily disabling MouseFollowsFocus if it's enabled.
    fn send_with_mouse_follow_off(monitor: &MonitorInfo, message: SocketMessage) -> IoResult<()> {
        let messages: &[SocketMessage] = if monitor.mouse_follows_focus {
            &[MouseFollowsFocus(false), message, MouseFollowsFocus(true)]
        } else {
            &[message]
        };
        Self::send_messages(messages)
    }

    /// Sends a batch of messages to Komorebi, logging errors on failure.
    fn send_messages(messages: &[SocketMessage]) -> IoResult<()> {
        komorebi_client::send_batch(messages).map_err(|err| {
            tracing::error!("Failed to send message(s): {:?}\nError: {}", messages, err);
            err
        })
    }
}

/// Workspace bar with a pre-selected render strategy for efficient
/// workspace display
#[derive(Clone, Debug)]
pub struct WorkspacesBar {
    /// Chosen rendering function for this widget
    renderer: fn(&Self, &Context, &mut Ui, &WorkspaceInfo),
    /// Text size (default: 12.5)
    text_size: Vec2,
    /// Icon size (default: 12.5 * 1.4)
    icon_size: Vec2,
}

impl WorkspacesBar {
    /// Creates a `WorkspacesBar` instance from a workspace configuration.
    ///
    /// Selects a render strategy based on the given display format
    /// for optimal performance. Returns `None` if the widget is disabled.
    fn try_from(value: KomorebiWorkspacesConfig) -> Option<Self> {
        use WorkspacesDisplayFormat::*;
        if !value.enable {
            return None;
        }
        // Selects a render strategy according to the workspace config's display format
        // for better performance
        let renderer: fn(&Self, &Context, &mut Ui, &WorkspaceInfo) =
            match value.display.unwrap_or(DisplayFormat::Text.into()) {
                // 1: Show icons if any, fallback if none | Only hover workspace name
                AllIcons | Existing(DisplayFormat::Icon) => |bar, ctx, ui, ws| {
                    bar.show_icons(ctx, ui, ws)
                        .unwrap_or_else(|| bar.show_fallback_icon(ctx, ui, ws))
                        .on_hover_text(&ws.name);
                },
                // 2: Show icons, with no fallback | Label workspace name (no hover)
                AllIconsAndText | Existing(DisplayFormat::IconAndText) => |bar, ctx, ui, ws| {
                    bar.show_icons(ctx, ui, ws);
                    Self::show_label(ctx, ui, ws);
                },
                // 3: Show icons, fallback if no icons and not selected | Label workspace name if selected else hover
                AllIconsAndTextOnSelected | Existing(DisplayFormat::IconAndTextOnSelected) => {
                    |bar, ctx, ui, ws| {
                        if bar.show_icons(ctx, ui, ws).is_none() && !ws.is_selected {
                            bar.show_fallback_icon(ctx, ui, ws);
                        }
                        if ws.is_selected {
                            Self::show_label(ctx, ui, ws);
                        } else {
                            ui.response().on_hover_text(&ws.name);
                        }
                    }
                }
                // 4: Show icons if selected and has icons (no fallback) | Label workspace name
                Existing(DisplayFormat::TextAndIconOnSelected) => |bar, ctx, ui, ws| {
                    if ws.is_selected {
                        bar.show_icons(ctx, ui, ws);
                    }
                    Self::show_label(ctx, ui, ws);
                },
                // 5: Never show icon (no icons at all) | Label workspace name always
                Existing(DisplayFormat::Text) => |_, ctx, ui, ws| {
                    Self::show_label(ctx, ui, ws);
                },
            };

        Some(Self {
            renderer,
            icon_size: Vec2::splat(12.5),
            text_size: Vec2::splat(12.5 * 1.4),
        })
    }

    /// Draws all window icons for the workspace, using larger size for the focused container.
    /// Returns response if icons exist, or None.
    fn show_icons(&self, ctx: &Context, ui: &mut Ui, ws: &WorkspaceInfo) -> Option<Response> {
        ws.has_icons.then(|| {
            Frame::NONE
                .inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
                .show(ui, |ui| {
                    for container in &ws.containers {
                        for icon in container.windows.iter().filter_map(|win| win.icon.as_ref()) {
                            ui.add(
                                Image::from(&icon.texture(ctx))
                                    .maintain_aspect_ratio(true)
                                    .fit_to_exact_size(if container.is_focused {
                                        self.icon_size
                                    } else {
                                        self.text_size
                                    }),
                            );
                        }
                    }
                })
                .response
        })
    }

    /// Draws a fallback icon (a rectangle with a diagonal) for the workspace.
    fn show_fallback_icon(&self, ctx: &Context, ui: &mut Ui, ws: &WorkspaceInfo) -> Response {
        let (response, painter) = ui.allocate_painter(self.icon_size, Sense::hover());
        let stroke: Stroke = Stroke::new(
            1.0_f32,
            if ws.is_selected {
                ctx.style().visuals.selection.stroke.color
            } else {
                ui.style().visuals.text_color()
            },
        );
        let mut rect = response.rect;
        let rounding = CornerRadius::same((rect.width() * 0.1) as u8);
        rect = rect.shrink(stroke.width);
        let c = rect.center();
        let r = rect.width() / 2.0;
        painter.rect_stroke(rect, rounding, stroke, StrokeKind::Outside);
        painter.line_segment([c - vec2(r, r), c + vec2(r, r)], stroke);
        response
    }

    /// Shows the workspace label (colored if selected).
    fn show_label(ctx: &Context, ui: &mut Ui, ws: &WorkspaceInfo) -> Response {
        if ws.is_selected {
            let text = RichText::new(&ws.name).color(ctx.style().visuals.selection.stroke.color);
            ui.add(Label::new(text).selectable(false))
        } else {
            ui.add(Label::new(&ws.name).selectable(false))
        }
    }
}

/// Structure for displaying the focused container's windows in the bar.
/// Uses a pre-selected rendering strategy (based on config) for efficient display
#[derive(Clone, Debug)]
pub struct FocusedContainerBar {
    /// Chosen rendering function for this widget
    renderer: fn(&Self, &Context, &mut Ui, &WindowInfo, Color32, focused: bool),
    /// Icon size (default: 12.5 * 1.4)
    icon_size: Vec2,
}

impl FocusedContainerBar {
    /// Creates a `FocusedContainerBar` from the given configuration.
    ///
    /// Selects a render strategy based on the display format or legacy icon setting.
    /// Returns `None` if the widget is disabled.
    fn try_from(value: KomorebiFocusedContainerConfig) -> Option<Self> {
        if !value.enable {
            return None;
        }

        // Handle legacy setting - convert show_icon to display format
        #[allow(deprecated)]
        let format = value
            .display
            .unwrap_or(if value.show_icon.unwrap_or(false) {
                IconAndText
            } else {
                Text
            });

        // Select renderer strategy based on display format for better performance
        let renderer: fn(&FocusedContainerBar, &Context, &mut Ui, &WindowInfo, Color32, bool) =
            match format {
                Icon => |_self, ctx, ui, info, _color, _focused| {
                    FocusedContainerBar::show_icon::<true>(_self, ctx, ui, info);
                },
                Text => |_self, _ctx, ui, info, color, _focused| {
                    FocusedContainerBar::show_title(_self, ui, info, color);
                },
                IconAndText => |_self, ctx, ui, info, color, _focused| {
                    FocusedContainerBar::show_icon::<false>(_self, ctx, ui, info);
                    FocusedContainerBar::show_title(_self, ui, info, color);
                },
                IconAndTextOnSelected => |_self, ctx, ui, info, color, focused| {
                    FocusedContainerBar::show_icon::<false>(_self, ctx, ui, info);
                    if focused {
                        FocusedContainerBar::show_title(_self, ui, info, color);
                    }
                },
                TextAndIconOnSelected => |_self, ctx, ui, info, color, focused| {
                    if focused {
                        FocusedContainerBar::show_icon::<false>(_self, ctx, ui, info);
                    }
                    FocusedContainerBar::show_title(_self, ui, info, color);
                },
            };

        Some(FocusedContainerBar {
            renderer,
            icon_size: Vec2::splat(12.5 * 1.4),
        })
    }

    /// Draws window icon(s) at configured size; adds hover text if HOVEL is true.
    fn show_icon<const HOVEL: bool>(&self, ctx: &Context, ui: &mut Ui, info: &WindowInfo) {
        let inner_response = Frame::NONE
            .inner_margin(Margin::same(ui.style().spacing.button_padding.y as i8))
            .show(ui, |ui| {
                for icon in info.icon.iter() {
                    let img = Image::from(&icon.texture(ctx)).maintain_aspect_ratio(true);
                    ui.add(img.fit_to_exact_size(self.icon_size));
                }
            });
        if HOVEL && let Some(title) = &info.title {
            inner_response.response.on_hover_text(title);
        }
    }

    /// Renders the window title label in color, truncating if needed.
    fn show_title(&self, ui: &mut Ui, info: &WindowInfo, color: Color32) {
        if let Some(title) = &info.title {
            let text = Label::new(RichText::new(title).color(color)).selectable(false);

            let x = MAX_LABEL_WIDTH.load(Ordering::SeqCst) as f32;
            let y = ui.available_height();
            CustomUi(ui).add_sized_left_to_right(Vec2::new(x, y), text.truncate());
        }
    }
}

/// Workspace layer bar with a pre-selected render strategy for efficient
/// layer display
#[derive(Clone, Debug)]
pub struct WorkspaceLayerBar {
    /// Show the widget even if the layer is Tiling
    show_when_tiling: bool,
    /// Chosen rendering function for this widget
    renderer: fn(&Context, &mut Ui, &WorkspaceLayer, Vec2),
}

impl WorkspaceLayerBar {
    /// Creates a `WorkspaceLayerBar` from the given configuration.
    ///
    /// Selects a render strategy based on the display format.
    /// Returns `None` if the widget is disabled.
    fn try_from(value: KomorebiWorkspaceLayerConfig) -> Option<Self> {
        if !value.enable {
            return None;
        }
        let display_format = value.display.unwrap_or(Text);

        // Select renderer strategy based on display format for better performance
        let renderer: fn(&Context, &mut Ui, &WorkspaceLayer, Vec2) = match display_format {
            Icon => Self::draw_layer_icon,
            Text => |_ctx, ui, layer, _size| {
                ui.add(Label::new(layer.to_string()).selectable(false));
            },
            _ => |ctx, ui, layer, size| {
                Self::draw_layer_icon(ctx, ui, layer, size);
                ui.add(Label::new(layer.to_string()).selectable(false));
            },
        };

        Some(Self {
            show_when_tiling: value.show_when_tiling.unwrap_or_default(),
            renderer,
        })
    }

    /// Draws an icon representing the current workspace layer.
    fn draw_layer_icon(ctx: &Context, ui: &mut Ui, layer: &WorkspaceLayer, size: Vec2) {
        if matches!(layer, WorkspaceLayer::Tiling) {
            let (response, painter) = ui.allocate_painter(size, Sense::hover());
            let color = ctx.style().visuals.selection.stroke.color;
            let stroke = Stroke::new(1.0_f32, color);
            let mut rect = response.rect;
            let corner = CornerRadius::same((rect.width() * 0.1) as u8);
            rect = rect.shrink(stroke.width);

            // tiling
            let mut rect_left = response.rect;
            rect_left.set_width(rect.width() * 0.48);
            rect_left.set_height(rect.height() * 0.98);
            let mut rect_right = rect_left;
            rect_left = rect_left.translate(Vec2::new(
                rect.width() * 0.01 + stroke.width,
                rect.width() * 0.01 + stroke.width,
            ));
            rect_right = rect_right.translate(Vec2::new(
                rect.width() * 0.51 + stroke.width,
                rect.width() * 0.01 + stroke.width,
            ));
            painter.rect_filled(rect_left, corner, color);
            painter.rect_stroke(rect_right, corner, stroke, StrokeKind::Outside);
        } else {
            let (response, painter) = ui.allocate_painter(size, Sense::hover());
            let color = ctx.style().visuals.selection.stroke.color;
            let stroke = Stroke::new(1.0_f32, color);
            let mut rect = response.rect;
            let corner = CornerRadius::same((rect.width() * 0.1) as u8);
            rect = rect.shrink(stroke.width);

            // floating
            let mut rect_left = response.rect;
            rect_left.set_width(rect.width() * 0.65);
            rect_left.set_height(rect.height() * 0.65);
            let mut rect_right = rect_left;
            rect_left = rect_left.translate(Vec2::new(
                rect.width() * 0.01 + stroke.width,
                rect.width() * 0.01 + stroke.width,
            ));
            rect_right = rect_right.translate(Vec2::new(
                rect.width() * 0.34 + stroke.width,
                rect.width() * 0.34 + stroke.width,
            ));
            painter.rect_filled(rect_left, corner, color);
            painter.rect_stroke(rect_right, corner, stroke, StrokeKind::Outside);
        }
    }
}

/// Locked container bar for displaying and interacting with container lock state
#[derive(Clone, Debug)]
pub struct LockedContainerBar {
    /// Show the widget even when unlocked
    show_when_unlocked: bool,
    /// Chosen rendering function for this widget
    renderer: fn(&Context, &mut Ui, bool, FontId, FontId),
}

impl LockedContainerBar {
    /// Creates a `LockedContainerBar` from the given configuration.
    ///
    /// Selects a render strategy based on the display format.
    /// Returns `None` if the widget is disabled.
    fn try_from(value: KomorebiLockedContainerConfig) -> Option<Self> {
        if !value.enable {
            return None;
        }
        let display_format = value.display.unwrap_or(DisplayFormat::Text);

        // Select renderer strategy based on display format for better performance
        let renderer: fn(&Context, &mut Ui, bool, FontId, FontId) = match display_format {
            DisplayFormat::Icon => |ctx, ui, is_locked, icon_font, _txt_font| {
                let layout_job = Self::icon_layout(ctx, is_locked, icon_font);
                ui.add(Label::new(layout_job).selectable(false));
            },
            DisplayFormat::Text => |ctx, ui, is_locked, _icon_font, txt_font| {
                let mut layout_job = LayoutJob::default();
                Self::append_text(&mut layout_job, ctx, is_locked, txt_font);
                ui.add(Label::new(layout_job).selectable(false));
            },
            _ => |ctx, ui: &mut Ui, is_locked, icon_font, txt_font| {
                let mut layout_job = Self::icon_layout(ctx, is_locked, icon_font);
                Self::append_text(&mut layout_job, ctx, is_locked, txt_font);
                ui.add(Label::new(layout_job).selectable(false));
            },
        };

        Some(Self {
            show_when_unlocked: value.show_when_unlocked.unwrap_or_default(),
            renderer,
        })
    }

    /// Builds a layout job for the lock/unlock icon,
    /// using the provided font and current lock state.
    fn icon_layout(ctx: &Context, is_locked: bool, icon_font: FontId) -> LayoutJob {
        LayoutJob::simple(
            if is_locked {
                egui_phosphor::regular::LOCK_KEY.to_string()
            } else {
                egui_phosphor::regular::LOCK_SIMPLE_OPEN.to_string()
            },
            icon_font,
            ctx.style().visuals.selection.stroke.color,
            100.0,
        )
    }

    /// Appends lock/unlock status text to the given layout job,
    /// using the specified font and current lock state.
    fn append_text(job: &mut LayoutJob, ctx: &Context, is_locked: bool, txt_font: FontId) {
        job.append(
            if is_locked { "Locked" } else { "Unlocked" },
            10.0,
            TextFormat {
                font_id: txt_font,
                color: ctx.style().visuals.text_color(),
                valign: Align::Center,
                ..Default::default()
            },
        )
    }
}

/// Represents the full state of a single monitor for the Komorebi bar/UI.
///
/// Includes all workspaces, containers, windows, layout, focus,
/// display options, and monitor indices. Used to render the current monitor
/// state in UI widgets.
///
/// Updated whenever Komorebi state changes.
#[derive(Clone, Debug)]
pub struct MonitorInfo {
    pub workspaces: Vec<WorkspaceInfo>,
    pub layout: KomorebiLayout,
    pub mouse_follows_focus: bool,
    pub work_area_offset: Option<Rect>,
    pub monitor_index: usize,
    pub monitor_usr_idx_map: HashMap<usize, usize>,
    pub focused_workspace_idx: Option<usize>,
    pub show_all_icons: bool,
    pub hide_empty_workspaces: bool,
}

impl Default for MonitorInfo {
    fn default() -> Self {
        Self {
            workspaces: Vec::new(),
            layout: KomorebiLayout::Default(komorebi_client::DefaultLayout::BSP),
            mouse_follows_focus: true,
            work_area_offset: None,
            monitor_index: MONITOR_INDEX.load(Ordering::SeqCst),
            monitor_usr_idx_map: HashMap::new(),
            focused_workspace_idx: None,
            show_all_icons: false,
            hide_empty_workspaces: false,
        }
    }
}

impl MonitorInfo {
    /// Returns a reference to the currently focused workspace, if any.
    pub fn focused_workspace(&self) -> Option<&WorkspaceInfo> {
        self.workspaces.get(self.focused_workspace_idx?)
    }

    /// Returns the layer of the focused workspace, if available.
    pub fn focused_workspace_layer(&self) -> Option<WorkspaceLayer> {
        self.focused_workspace().map(|ws| ws.layer)
    }

    /// Returns the focused container within the focused workspace, if any.
    pub fn focused_container(&self) -> Option<&ContainerInfo> {
        self.focused_workspace()
            .and_then(WorkspaceInfo::focused_container)
    }
}

impl MonitorInfo {
    pub fn update_from_self(&mut self, config: &Self) {
        self.hide_empty_workspaces = config.hide_empty_workspaces;
    }

    /// Updates monitor state from the given State, setting all fields based on the selected
    /// monitor and its workspaces
    pub fn update(&mut self, monitor_index: Option<usize>, state: State, show_all_icons: bool) {
        self.show_all_icons = show_all_icons;
        self.monitor_usr_idx_map = state.monitor_usr_idx_map;

        match monitor_index {
            Some(idx) if idx < state.monitors.elements().len() => self.monitor_index = idx,
            // The bar's monitor is diconnected, so the bar is disabled no need to check anything
            // any further otherwise we'll get `OutOfBounds` panics.
            _ => return,
        };
        self.mouse_follows_focus = state.mouse_follows_focus;

        let monitor = &state.monitors.elements()[self.monitor_index];
        self.work_area_offset = monitor.work_area_offset;
        self.focused_workspace_idx = Some(monitor.focused_workspace_idx());

        // Layout
        let focused_ws = &monitor.workspaces()[monitor.focused_workspace_idx()];
        self.layout = Self::resolve_layout(focused_ws, state.is_paused);

        self.workspaces.clear();
        self.workspaces.extend(Self::workspaces(
            self.show_all_icons,
            self.hide_empty_workspaces,
            self.focused_workspace_idx,
            monitor.workspaces().iter().enumerate(),
        ));
    }

    /// Builds an iterator of WorkspaceInfo for the monitor.
    fn workspaces<'a, I>(
        show_all_icons: bool,
        hide_empty_ws: bool,
        focused_ws_idx: Option<usize>,
        iter: I,
    ) -> impl Iterator<Item = WorkspaceInfo> + 'a
    where
        I: Iterator<Item = (usize, &'a Workspace)> + 'a,
    {
        let fn_containers_from = if show_all_icons {
            |ws| ContainerInfo::from_all_containers(ws)
        } else {
            |ws| {
                ContainerInfo::from_focused_container(ws)
                    .into_iter()
                    .collect()
            }
        };
        iter.map(move |(index, ws)| {
            let containers = fn_containers_from(ws);
            WorkspaceInfo {
                name: ws
                    .name
                    .to_owned()
                    .unwrap_or_else(|| format!("{}", index + 1)),
                focused_container_idx: containers.iter().position(|c| c.is_focused),
                has_icons: containers
                    .iter()
                    .any(|container| container.windows.iter().any(|window| window.icon.is_some())),
                containers,
                layer: ws.layer,
                should_show: !hide_empty_ws || focused_ws_idx == Some(index) || !ws.is_empty(),
                is_selected: focused_ws_idx == Some(index),
            }
        })
    }

    /// Determines the current layout of the focused workspace
    fn resolve_layout(focused_ws: &Workspace, is_paused: bool) -> KomorebiLayout {
        if focused_ws.monocle_container.is_some() {
            KomorebiLayout::Monocle
        } else if !focused_ws.tile {
            KomorebiLayout::Floating
        } else if is_paused {
            KomorebiLayout::Paused
        } else {
            match focused_ws.layout {
                komorebi_client::Layout::Default(layout) => KomorebiLayout::Default(layout),
                komorebi_client::Layout::Custom(_) => KomorebiLayout::Custom,
            }
        }
    }
}

/// Describes the state of a single workspace on a monitor.
///
/// Contains the workspace name, its containers (tiled, floating,
/// monocle), focus state, layer (tiling/floating), and display
/// flags for UI rendering.
#[derive(Clone, Debug)]
pub struct WorkspaceInfo {
    pub name: String,
    pub containers: Vec<ContainerInfo>,
    pub focused_container_idx: Option<usize>,
    pub layer: WorkspaceLayer,
    pub should_show: bool,
    pub is_selected: bool,
    pub has_icons: bool,
}

impl WorkspaceInfo {
    /// Returns a reference to the focused container in this workspace, if any.
    pub fn focused_container(&self) -> Option<&ContainerInfo> {
        self.containers.get(self.focused_container_idx?)
    }
}

/// Holds information about a window container (tiled, floating, or monocle)
/// within a workspace.
///
/// Includes a list of windows, the focused window index, and flags for focus
/// and lock status.
#[derive(Clone, Debug)]
pub struct ContainerInfo {
    pub windows: Vec<WindowInfo>,
    pub focused_window_idx: usize,
    pub is_focused: bool,
    pub is_locked: bool,
}

impl ContainerInfo {
    /// Returns all containers for the given workspace in the following order:
    ///
    /// 1. The monocle container (if present) is included first.
    /// 2. All tiled containers are included next.
    /// 3. All floating windows are added last, each as a separate container.
    ///
    /// Function ensures only one container is marked as focused, prioritizing
    /// floating → monocle → tiled.
    pub fn from_all_containers(ws: &Workspace) -> Vec<Self> {
        let has_focused_float = ws.floating_windows().iter().any(|w| w.is_focused());

        // Monocle container first if present
        let monocle = ws
            .monocle_container
            .as_ref()
            .map(|c| Self::from_container(c, !has_focused_float));

        // All tiled containers, focus only if there's no monocle/focused float
        let has_focused_monocle_or_float = has_focused_float || monocle.is_some();
        let tiled = ws.containers().iter().enumerate().map(|(i, c)| {
            let is_focused = !has_focused_monocle_or_float && i == ws.focused_container_idx();
            Self::from_container(c, is_focused)
        });

        // All floating windows
        let floats = ws.floating_windows().iter().map(Self::from_window);
        // All windows
        monocle.into_iter().chain(tiled).chain(floats).collect()
    }

    /// Creates a `ContainerInfo` for the currently focused item in the workspace.
    ///
    /// The function checks focus in the following order:
    /// 1. Focused floating window
    /// 2. Monocle container
    /// 3. Focused tiled container
    pub fn from_focused_container(ws: &Workspace) -> Option<Self> {
        if let Some(window) = ws.floating_windows().iter().find(|w| w.is_focused()) {
            return Some(Self::from_window(window));
        }
        if let Some(container) = &ws.monocle_container {
            Some(Self::from_container(container, true))
        } else {
            ws.focused_container()
                .map(|container| Self::from_container(container, true))
        }
    }

    /// Creates a `ContainerInfo` from a given container.
    pub fn from_container(container: &Container, is_focused: bool) -> Self {
        Self {
            windows: container.windows().iter().map(WindowInfo::from).collect(),
            focused_window_idx: container.focused_window_idx(),
            is_focused,
            is_locked: container.locked,
        }
    }

    /// Creates a `ContainerInfo` from a single floating window.
    /// The window becomes the only entry in `windows`, is marked as focused
    /// if applicable, and `is_locked` is set to false.
    pub fn from_window(window: &Window) -> Self {
        Self {
            windows: vec![window.into()],
            focused_window_idx: 0,
            is_focused: window.is_focused(),
            is_locked: false, // locked is only container feature
        }
    }
}

/// Stores basic information about a single window in a container.
/// Contains the window's title and its icon, if available.
#[derive(Clone, Debug)]
pub struct WindowInfo {
    pub title: Option<String>,
    pub icon: Option<ImageIcon>,
}

impl From<&Window> for WindowInfo {
    fn from(value: &Window) -> Self {
        Self {
            title: value.title().ok(),
            icon: ImageIcon::try_load(value.hwnd, || {
                windows_icons::get_icon_by_hwnd(value.hwnd)
                    .or_else(|| windows_icons_fallback::get_icon_by_process_id(value.process_id()))
            }),
        }
    }
}
