// Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: GPL-3.0-only

mod localize;
pub(crate) mod wayland_handler;
pub(crate) mod wayland_subscription;
pub(crate) mod window_image;

use std::borrow::Cow;

use crate::localize::localize;
use cosmic::{
    Task, app,
    applet::cosmic_panel_config::PanelAnchor,
    cctk::{
        sctk::reexports::calloop, toplevel_info::ToplevelInfo,
        wayland_protocols::ext::foreign_toplevel_list::v1::client::ext_foreign_toplevel_handle_v1::ExtForeignToplevelHandleV1,
    },
    desktop::fde,
    iced::{
        self, Length, Limits, Subscription,
        id::Id as WidgetId,
        platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup},
        widget::text,
        window::{self},
    },
    surface,
    widget::{autosize::autosize, mouse_area},
};

use cosmic::iced::widget::{Column, Row};

use cosmic::{Element, widget::tooltip};
use std::sync::LazyLock;
use wayland_subscription::{
    ToplevelRequest, ToplevelUpdate, WaylandImage, WaylandRequest, WaylandUpdate,
};

static AUTOSIZE_MAIN_ID: LazyLock<WidgetId> = LazyLock::new(|| WidgetId::new("autosize-main"));

pub fn run() -> cosmic::iced::Result {
    localize();
    cosmic::applet::run::<Minimize>(())
}

pub struct App {
    desktop_entry: fde::DesktopEntry,
    name: String,
    icon_source: fde::IconSource,
    toplevel_info: ToplevelInfo,
    wayland_image: Option<WaylandImage>,
}

#[derive(Default)]
struct Minimize {
    core: cosmic::app::Core,
    locales: Vec<String>,
    desktop_entries: Vec<fde::DesktopEntry>,
    apps: Vec<App>,
    tx: Option<calloop::channel::Sender<WaylandRequest>>,
    overflow_popup: Option<window::Id>,
}

impl Minimize {
    fn max_icon_count(&self) -> Option<usize> {
        let mut index = None;
        let Some(max_major_axis_len) = self.core.applet.suggested_bounds.as_ref().map(|c| {
            // if we have a configure for width and height, we're in a overflow popup
            match self.core.applet.anchor {
                PanelAnchor::Top | PanelAnchor::Bottom => c.width as u32,
                PanelAnchor::Left | PanelAnchor::Right => c.height as u32,
            }
        }) else {
            return index;
        };
        let button_total_size = self.core.applet.suggested_size(true).0
            + self.core.applet.suggested_padding(true).0 * 2
            + 4;
        let btn_count = max_major_axis_len / button_total_size as u32;
        if btn_count >= self.apps.len() as u32 {
            index = None;
        } else {
            index = Some((btn_count as usize).max(2).min(self.apps.len()));
        }
        index
    }

    fn find_new_desktop_entry(&mut self, appid: &str) -> fde::DesktopEntry {
        let unicase_appid = fde::unicase::Ascii::new(appid);

        let de = if let Some(de) = fde::find_app_by_id(&self.desktop_entries, unicase_appid) {
            de
        } else {
            // Update desktop entries in case it was not found.
            self.update_desktop_entries();
            if let Some(appid) = fde::find_app_by_id(&self.desktop_entries, unicase_appid) {
                appid
            } else {
                tracing::warn!(appid, "could not find desktop entry for app");
                let mut entry = fde::DesktopEntry {
                    appid: appid.to_owned(),
                    groups: Default::default(),
                    path: Default::default(),
                    ubuntu_gettext_domain: None,
                };
                entry.add_desktop_entry("Name".to_string(), appid.to_owned());
                return entry;
            }
        };

        de.clone()
    }

    // Cache all desktop entries to use when new apps are added to the dock.
    fn update_desktop_entries(&mut self) {
        self.desktop_entries = fde::Iter::new(fde::default_paths())
            .filter_map(|p| fde::DesktopEntry::from_path(p, Some(&self.locales)).ok())
            .collect::<Vec<_>>();
    }
}

#[derive(Debug, Clone)]
enum Message {
    Wayland(WaylandUpdate),
    Activate(ExtForeignToplevelHandleV1),
    Closed(window::Id),
    OpenOverflowPopup,
    CloseOverflowPopup,
    Surface(surface::Action),
}

impl cosmic::Application for Minimize {
    type Message = Message;
    type Executor = cosmic::SingleThreadExecutor;
    type Flags = ();
    const APP_ID: &'static str = "com.system76.CosmicAppletMinimize";

    fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
        let mut app = Self {
            core,
            locales: fde::get_languages_from_env(),
            ..Default::default()
        };

        app.update_desktop_entries();
        let t = iced::window::minimize::<cosmic::Action<Message>>(
            app.core.main_window_id().unwrap(),
            true,
        );
        (app, t)
    }

    fn core(&self) -> &cosmic::app::Core {
        &self.core
    }

    fn core_mut(&mut self) -> &mut cosmic::app::Core {
        &mut self.core
    }

    fn style(&self) -> Option<iced::theme::Style> {
        Some(cosmic::applet::style())
    }

    fn update(&mut self, message: Message) -> app::Task<Message> {
        match message {
            Message::Wayland(update) => match update {
                WaylandUpdate::Init(tx) => {
                    self.tx = Some(tx);
                }
                WaylandUpdate::Finished => {
                    panic!("Wayland Subscription ended...")
                }
                WaylandUpdate::Toplevel(t) => match t {
                    ToplevelUpdate::Add(toplevel_info) | ToplevelUpdate::Update(toplevel_info) => {
                        // Temporarily take ownership to appease the borrow checker.
                        let mut apps = std::mem::take(&mut self.apps);

                        if let Some(pos) = apps.iter().position(|a| {
                            a.toplevel_info.foreign_toplevel == toplevel_info.foreign_toplevel
                        }) {
                            if apps[pos].toplevel_info.app_id != toplevel_info.app_id {
                                apps[pos].desktop_entry =
                                    self.find_new_desktop_entry(&toplevel_info.app_id);
                                apps[pos].icon_source = fde::IconSource::from_unknown(
                                    apps[pos]
                                        .desktop_entry
                                        .icon()
                                        .unwrap_or(&apps[pos].desktop_entry.appid),
                                );
                            }
                            apps[pos].toplevel_info = toplevel_info;
                        } else {
                            let desktop_entry = self.find_new_desktop_entry(&toplevel_info.app_id);

                            apps.push(App {
                                name: desktop_entry
                                    .full_name(&self.locales)
                                    .unwrap_or(Cow::Borrowed(&desktop_entry.appid))
                                    .into_owned(),
                                icon_source: fde::IconSource::from_unknown(
                                    desktop_entry.icon().unwrap_or(&desktop_entry.appid),
                                ),
                                desktop_entry,
                                toplevel_info,
                                wayland_image: None,
                            });
                        }

                        self.apps = apps;

                        return iced::window::maximize(self.core.main_window_id().unwrap(), true);
                    }
                    ToplevelUpdate::Remove(handle) => {
                        let prev_was_empty = self.apps.is_empty();
                        self.apps
                            .retain(|a| a.toplevel_info.foreign_toplevel != handle);
                        self.apps.shrink_to_fit();
                        let changed = prev_was_empty != self.apps.is_empty();
                        if self.apps.is_empty() && changed {
                            // hide the window
                            return iced::window::minimize(
                                self.core.main_window_id().unwrap(),
                                true,
                            );
                        }
                    }
                },
                WaylandUpdate::Image(handle, img) => {
                    if let Some(pos) = self
                        .apps
                        .iter()
                        .position(|a| a.toplevel_info.foreign_toplevel == handle)
                    {
                        self.apps[pos].wayland_image = Some(img);
                    }
                }
            },
            Message::Activate(handle) => {
                if let Some(tx) = self.tx.as_ref() {
                    let _ = tx.send(WaylandRequest::Toplevel(ToplevelRequest::Activate(handle)));
                }
            }
            Message::Closed(id) => {
                if self.overflow_popup.is_some_and(|i| i == id) {
                    self.overflow_popup = None;
                }
            }
            Message::OpenOverflowPopup => {
                if let Some(id) = self.overflow_popup.take() {
                    return cosmic::surface::surface_task(cosmic::surface::action::destroy_popup(
                        id,
                    ));
                } else {
                    return cosmic::surface::surface_task(cosmic::surface::action::app_popup(
                        |_| Default::default(),
                        |app: &mut Self| {
                            let new_id = window::Id::unique();
                            let pos = app.max_icon_count().unwrap_or_default();

                            app.overflow_popup = Some(new_id);
                            let icon_size = app.core.applet.suggested_size(true).0 as u32
                                + 2 * app.core.applet.suggested_padding(true).1 as u32;
                            let spacing = app.core.system_theme().cosmic().space_xxs() as u32;
                            let major_axis_len =
                                (icon_size + spacing) * (pos.saturating_sub(1) as u32);
                            let rectangle = match app.core.applet.anchor {
                                PanelAnchor::Top | PanelAnchor::Bottom => iced::Rectangle {
                                    x: major_axis_len as i32,
                                    y: 0,
                                    width: icon_size as i32,
                                    height: icon_size as i32,
                                },
                                PanelAnchor::Left | PanelAnchor::Right => iced::Rectangle {
                                    x: 0,
                                    y: major_axis_len as i32,
                                    width: icon_size as i32,
                                    height: icon_size as i32,
                                },
                            };
                            let mut popup_settings = app.core.applet.get_popup_settings(
                                app.core.main_window_id().unwrap(),
                                new_id,
                                None,
                                None,
                                None,
                            );
                            popup_settings.positioner.anchor_rect = rectangle;
                            popup_settings
                        },
                        None,
                    ));
                }
            }
            Message::CloseOverflowPopup => todo!(),
            Message::Surface(a) => {
                return cosmic::task::message(cosmic::Action::Cosmic(
                    cosmic::app::Action::Surface(a),
                ));
            }
        }
        Task::none()
    }

    fn subscription(&self) -> Subscription<Message> {
        wayland_subscription::wayland_subscription().map(Message::Wayland)
    }

    fn view(&self) -> Element<'_, Self::Message> {
        let max_icon_count = self.max_icon_count().map_or(self.apps.len(), |n| {
            if n < self.apps.len() {
                n - 1
            } else {
                self.apps.len()
            }
        });
        let (width, _) = self.core.applet.suggested_size(false);
        let (major_padding, cross_padding) = self.core.applet.suggested_padding(false);
        let padding = if matches!(
            self.core.applet.anchor,
            PanelAnchor::Top | PanelAnchor::Bottom
        ) {
            (major_padding, cross_padding)
        } else {
            (cross_padding, major_padding)
        };
        let theme = self.core.system_theme().cosmic();
        let icon_buttons = self.apps[..max_icon_count].iter().map(|app| {
            self.core
                .applet
                .applet_tooltip(
                    Element::from(crate::window_image::WindowImage::new(
                        app.wayland_image.clone(),
                        &app.icon_source,
                        width as f32,
                        Message::Activate(app.toplevel_info.foreign_toplevel.clone()),
                        padding,
                    )),
                    app.name.clone(),
                    self.overflow_popup.is_some(),
                    Message::Surface,
                    None,
                )
                .into()
        });
        let overflow_btn = if max_icon_count < self.apps.len() {
            let icon = match self.core.applet.anchor {
                PanelAnchor::Bottom => "go-up-symbolic",
                PanelAnchor::Left => "go-next-symbolic",
                PanelAnchor::Right => "go-previous-symbolic",
                PanelAnchor::Top => "go-down-symbolic",
            };
            let btn = self
                .core
                .applet
                .icon_button(icon)
                .on_press_down(Message::OpenOverflowPopup);

            Some(btn.into())
        } else {
            None
        };

        let spacing = self.core.applet.spacing;
        // TODO optional dividers on ends if detects app list neighbor
        // not sure the best way to tell if there is an adjacent app-list
        let icon_buttons = icon_buttons.chain(overflow_btn);
        let content: Element<_> = if matches!(
            self.core.applet.anchor,
            PanelAnchor::Top | PanelAnchor::Bottom
        ) {
            Row::with_children(icon_buttons)
                .align_y(cosmic::iced::core::Alignment::Center)
                .height(Length::Shrink)
                .width(Length::Shrink)
                .spacing(spacing as f32)
                .into()
        } else {
            Column::with_children(icon_buttons)
                .align_x(cosmic::iced::core::Alignment::Center)
                .height(Length::Shrink)
                .width(Length::Shrink)
                .spacing(spacing as f32)
                .into()
        };

        let mut limits = Limits::NONE.min_width(1.).min_height(1.);

        if let Some(b) = self.core.applet.suggested_bounds {
            if b.width as i32 > 0 {
                limits = limits.max_width(b.width);
            }
            if b.height as i32 > 0 {
                limits = limits.max_height(b.height);
            }
        }

        autosize(
            if self.overflow_popup.is_some() {
                mouse_area(content)
                    .on_press(Message::CloseOverflowPopup)
                    .into()
            } else {
                content
            },
            AUTOSIZE_MAIN_ID.clone(),
        )
        .limits(limits)
        .into()
    }

    fn view_window(&self, _id: window::Id) -> Element<'_, Self::Message> {
        let max_icon_count = self.max_icon_count().map_or(self.apps.len(), |n| {
            if n < self.apps.len() {
                n - 1
            } else {
                self.apps.len()
            }
        });
        let (width, _) = self.core.applet.suggested_size(false);
        let (major_padding, cross_padding) = self.core.applet.suggested_padding(false);
        let padding = if matches!(
            self.core.applet.anchor,
            PanelAnchor::Top | PanelAnchor::Bottom
        ) {
            (major_padding, cross_padding)
        } else {
            (cross_padding, major_padding)
        };
        let theme = self.core.system_theme().cosmic();
        let space_xxs = theme.space_xxs();
        let icon_buttons = self.apps[max_icon_count..].iter().map(|app| {
            tooltip(
                Element::from(crate::window_image::WindowImage::new(
                    app.wayland_image.clone(),
                    &app.icon_source,
                    width as f32,
                    Message::Activate(app.toplevel_info.foreign_toplevel.clone()),
                    padding,
                )),
                text(&app.name).shaping(text::Shaping::Advanced),
                // tooltip::Position::FollowCursor,
                // FIXME tooltip fails to appear when created as indicated in design
                // maybe it should be a subsurface
                match self.core.applet.anchor {
                    PanelAnchor::Left => tooltip::Position::Right,
                    PanelAnchor::Right => tooltip::Position::Left,
                    PanelAnchor::Top => tooltip::Position::Bottom,
                    PanelAnchor::Bottom => tooltip::Position::Top,
                },
            )
            .snap_within_viewport(false)
            .into()
        });

        // TODO optional dividers on ends if detects app list neighbor
        // not sure the best way to tell if there is an adjacent app-list

        self.core
            .applet
            .popup_container(
                if matches!(
                    self.core.applet.anchor,
                    PanelAnchor::Top | PanelAnchor::Bottom
                ) {
                    Element::from(
                        Row::with_children(icon_buttons)
                            .align_y(cosmic::iced::core::Alignment::Center)
                            .height(Length::Shrink)
                            .width(Length::Shrink),
                    )
                } else {
                    Column::with_children(icon_buttons)
                        .align_x(cosmic::iced::core::Alignment::Center)
                        .height(Length::Shrink)
                        .width(Length::Shrink)
                        .into()
                },
            )
            .into()
    }

    fn on_close_requested(&self, id: window::Id) -> Option<Self::Message> {
        Some(Message::Closed(id))
    }
}
