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

mod localize;
mod model;
mod mouse_area;

use crate::localize::localize;
use config::{AudioAppletConfig, amplification_sink, amplification_source};
use cosmic::{
    Apply, Element, Renderer, Task, Theme, app,
    applet::{
        column as applet_column,
        cosmic_panel_config::PanelAnchor,
        menu_button, menu_control_padding, padded_control, row as applet_row,
        token::subscription::{TokenRequest, TokenUpdate, activation_token_subscription},
    },
    cctk::sctk::reexports::calloop,
    cosmic_config::CosmicConfigEntry,
    cosmic_theme::Spacing,
    iced::{
        self, Alignment, Length, Rectangle, Subscription,
        futures::StreamExt,
        widget::{self, column, row, slider},
        window,
    },
    surface, theme,
    widget::{
        Row, button, container, divider, icon,
        rectangle_tracker::{RectangleTracker, RectangleUpdate, rectangle_tracker_subscription},
        space, text, toggler,
    },
};
use cosmic_settings_audio_client::{self as audio_client, CosmicAudioProxy};
use futures::SinkExt;
use iced::platform_specific::shell::wayland::commands::popup::{destroy_popup, get_popup};
use mpris_subscription::{MprisRequest, MprisUpdate};
use mpris2_zbus::player::PlaybackStatus;
use std::{cell::RefCell, rc::Rc, sync::Arc};

mod config;
mod mpris_subscription;

const GO_BACK: &str = "media-skip-backward-symbolic";
const GO_NEXT: &str = "media-skip-forward-symbolic";
const PAUSE: &str = "media-playback-pause-symbolic";
const PLAY: &str = "media-playback-start-symbolic";

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

#[derive(Default)]
pub struct Audio {
    /// For interfacing with libcosmic.
    core: cosmic::app::Core,
    /// Track the applet's popup window.
    popup: Option<window::Id>,
    /// Varlink connection to `com.system76.CosmicSettings.Audio`.
    audio_client: Option<Rc<RefCell<audio_client::Client>>>,
    /// Known audio device state
    model: model::Model,
    /// Whether to expand the revealer of a source or sink device.
    is_open: IsOpen,
    /// Max slider volume for the sink device, as determined by the amplification property.
    max_sink_volume: u32,
    /// Max slider volume for the source device, as determined by the amplification property.
    max_source_volume: u32,
    /// Breakpoints for the sink volume slider.
    sink_breakpoints: &'static [u32],
    /// Breakpoitns for the source volume slider.
    source_breakpoints: &'static [u32],
    /// Config file specific to this applet.
    config: AudioAppletConfig,
    /// mpris player status
    player_status: Option<mpris_subscription::PlayerStatus>,
    /// Used to request an activation token for opening cosmic-settings.
    token_tx: Option<calloop::channel::Sender<TokenRequest>>,
    rectangle_tracker: Option<RectangleTracker<u32>>,
    rectangle: Option<iced::Rectangle>,
}

impl Audio {
    fn output_icon_name(&self) -> &'static str {
        let volume = self.model.active_sink.volume;
        let mute = self.model.active_sink.mute;
        if mute || volume == 0 {
            "audio-volume-muted-symbolic"
        } else if volume < 33 {
            "audio-volume-low-symbolic"
        } else if volume < 66 {
            "audio-volume-medium-symbolic"
        } else if volume <= 100 {
            "audio-volume-high-symbolic"
        } else {
            "audio-volume-overamplified-symbolic"
        }
    }

    fn input_icon_name(&self) -> &'static str {
        let volume = self.model.active_source.volume;
        let mute = self.model.active_source.mute;
        if mute || volume == 0 {
            "microphone-sensitivity-muted-symbolic"
        } else if volume < 33 {
            "microphone-sensitivity-low-symbolic"
        } else if volume < 66 {
            "microphone-sensitivity-medium-symbolic"
        } else {
            "microphone-sensitivity-high-symbolic"
        }
    }
}

#[derive(Debug, PartialEq, Eq, Default)]
enum IsOpen {
    #[default]
    None,
    Output,
    Input,
}

#[derive(Clone, Debug)]
pub enum Message {
    /// Connection to `com.system76.CosmicSettings`.
    Client(Arc<audio_client::Client>),
    Ignore,
    SetSinkVolume(u32),
    SetSourceVolume(u32),
    ToggleSinkMute,
    ToggleSourceMute,
    SetDefaultSink(usize),
    SetDefaultSource(usize),
    OutputToggle,
    InputToggle,
    TogglePopup,
    CloseRequested(window::Id),
    ToggleMediaControlsInTopPanel(bool),
    ConfigChanged(AudioAppletConfig),
    Mpris(mpris_subscription::MprisUpdate),
    MprisRequest(MprisRequest),
    Token(TokenUpdate),
    OpenSettings,
    Subscription(audio_client::Event),
    Surface(surface::Action),
    Rectangle(RectangleUpdate<u32>),
}

// TODO
// mouse area with on enter and a stack widget for all buttons
// most recently entered button is on top
// position is a multiple of button size
// on leave of applet, popup button is on top again

impl Audio {
    fn playback_buttons(&self) -> Vec<Element<'_, Message>> {
        let mut elements: Vec<Element<'_, Message>> = Vec::new();
        if self.player_status.is_some() && self.config.show_media_controls_in_top_panel {
            if self
                .player_status
                .as_ref()
                .is_some_and(|s| s.can_go_previous)
            {
                elements.push(
                    self.core
                        .applet
                        .icon_button(GO_BACK)
                        .on_press(Message::MprisRequest(MprisRequest::Previous))
                        .into(),
                );
            }
            if let Some(play) = self.is_play() {
                elements.push(
                    self.core
                        .applet
                        .icon_button(if play { PLAY } else { PAUSE })
                        .on_press(if play {
                            Message::MprisRequest(MprisRequest::Play)
                        } else {
                            Message::MprisRequest(MprisRequest::Pause)
                        })
                        .into(),
                );
            }
            if self.player_status.as_ref().is_some_and(|s| s.can_go_next) {
                elements.push(
                    self.core
                        .applet
                        .icon_button(GO_NEXT)
                        .on_press(Message::MprisRequest(MprisRequest::Next))
                        .into(),
                )
            }
        }
        elements
    }

    fn go_previous(&self, icon_size: u16) -> Option<Element<'_, Message>> {
        self.player_status.as_ref().and_then(|s| {
            if s.can_go_previous {
                Some(
                    button::icon(icon::from_name(GO_BACK).size(icon_size).symbolic(true))
                        .extra_small()
                        .class(cosmic::theme::Button::AppletIcon)
                        .on_press(Message::MprisRequest(MprisRequest::Previous))
                        .into(),
                )
            } else {
                None
            }
        })
    }

    fn go_next(&self, icon_size: u16) -> Option<Element<'_, Message>> {
        self.player_status.as_ref().and_then(|s| {
            if s.can_go_next {
                Some(
                    button::icon(icon::from_name(GO_NEXT).size(icon_size).symbolic(true))
                        .extra_small()
                        .class(cosmic::theme::Button::AppletIcon)
                        .on_press(Message::MprisRequest(MprisRequest::Next))
                        .into(),
                )
            } else {
                None
            }
        })
    }

    fn is_play(&self) -> Option<bool> {
        self.player_status.as_ref().and_then(|s| match s.status {
            PlaybackStatus::Playing => {
                if s.can_pause {
                    Some(false)
                } else {
                    None
                }
            }

            PlaybackStatus::Paused | PlaybackStatus::Stopped => {
                if s.can_play {
                    Some(true)
                } else {
                    None
                }
            }
        })
    }
}

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

    fn init(core: cosmic::app::Core, _flags: ()) -> (Self, app::Task<Message>) {
        (
            Self {
                core,
                ..Default::default()
            },
            Task::none(),
        )
    }

    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::Rectangle(u) => match u {
                RectangleUpdate::Rectangle(r) => {
                    self.rectangle = Some(r.1);
                }
                RectangleUpdate::Init(tracker) => {
                    self.rectangle_tracker.replace(tracker);
                }
            },
            Message::Ignore => {}
            Message::TogglePopup => {
                if let Some(p) = self.popup.take() {
                    return cosmic::surface::surface_task(cosmic::surface::action::destroy_popup(
                        p,
                    ));
                } else {
                    return cosmic::surface::surface_task(cosmic::surface::action::app_popup(
                        |_| Default::default(),
                        |app: &mut Self| {
                            let new_id = window::Id::unique();
                            app.popup.replace(new_id);

                            (app.max_sink_volume, app.sink_breakpoints) = if amplification_sink() {
                                (150, &[100][..])
                            } else {
                                (100, &[][..])
                            };

                            (app.max_source_volume, app.source_breakpoints) =
                                if amplification_source() {
                                    (150, &[100][..])
                                } else {
                                    (100, &[][..])
                                };

                            let mut popup_settings = app.core.applet.get_popup_settings(
                                app.core.main_window_id().unwrap(),
                                new_id,
                                None,
                                None,
                                None,
                            );
                            if let Some(r) = app.rectangle {
                                popup_settings.positioner.anchor_rect = Rectangle {
                                    x: r.x as i32,
                                    y: r.y as i32,
                                    width: r.width as i32,
                                    height: r.height as i32,
                                };
                            }
                            popup_settings
                        },
                        None,
                    ));
                }
            }

            Message::OutputToggle => {
                self.is_open = if self.is_open == IsOpen::Output {
                    IsOpen::None
                } else {
                    IsOpen::Output
                }
            }
            Message::InputToggle => {
                self.is_open = if self.is_open == IsOpen::Input {
                    IsOpen::None
                } else {
                    IsOpen::Input
                }
            }
            Message::Subscription(message) => {
                self.model.update(message);
            }

            Message::SetDefaultSink(pos) => {
                if let Some(&pos) = self.model.sinks.sorted_index.get(pos)
                    && let Some(&node_id) = self.model.sinks.id.get(pos as usize)
                    && let Some(client) = self.audio_client.as_mut()
                {
                    futures::executor::block_on(async {
                        _ = client.borrow_mut().conn.set_default(node_id, true).await;
                    });
                }
            }

            Message::SetDefaultSource(pos) => {
                if let Some(&pos) = self.model.sources.sorted_index.get(pos)
                    && let Some(&node_id) = self.model.sources.id.get(pos as usize)
                    && let Some(client) = self.audio_client.as_mut()
                {
                    futures::executor::block_on(async {
                        _ = client.borrow_mut().conn.set_default(node_id, true).await;
                    });
                }
            }

            Message::ToggleSinkMute => {
                if let Some(ref mut client) = self.audio_client {
                    futures::executor::block_on(async {
                        _ = client.borrow_mut().conn.sink_mute_toggle().await;
                    });
                }
            }

            Message::ToggleSourceMute => {
                if let Some(ref mut client) = self.audio_client {
                    futures::executor::block_on(async {
                        _ = client.borrow_mut().conn.source_mute_toggle().await;
                    });
                }
            }

            Message::SetSinkVolume(volume) => {
                if let Some(ref mut client) = self.audio_client {
                    self.model.active_sink.volume = volume;
                    self.model.active_sink.volume_text = volume.to_string();
                    futures::executor::block_on(async {
                        _ = client.borrow_mut().conn.set_sink_volume(volume).await;
                    });
                }
            }

            Message::SetSourceVolume(volume) => {
                if let Some(ref mut client) = self.audio_client {
                    self.model.active_source.volume = volume;
                    self.model.active_source.volume_text = volume.to_string();
                    futures::executor::block_on(async {
                        _ = client.borrow_mut().conn.set_source_volume(volume).await;
                    });
                }
            }

            Message::ToggleMediaControlsInTopPanel(enabled) => {
                self.config.show_media_controls_in_top_panel = enabled;
                if let Ok(helper) =
                    cosmic::cosmic_config::Config::new(Self::APP_ID, AudioAppletConfig::VERSION)
                    && let Err(err) = self.config.write_entry(&helper)
                {
                    tracing::error!(?err, "Error writing config");
                }
            }
            Message::CloseRequested(id) => {
                if Some(id) == self.popup {
                    self.popup = None;
                }
            }
            Message::ConfigChanged(c) => {
                self.config = c;
            }
            Message::Mpris(mpris_subscription::MprisUpdate::Player(p)) => {
                self.player_status = Some(p);
            }
            Message::Mpris(MprisUpdate::Finished) => {
                self.player_status = None;
            }
            Message::Mpris(MprisUpdate::Setup) => {
                self.player_status = None;
            }
            Message::MprisRequest(r) => {
                let Some(player_status) = self.player_status.as_ref() else {
                    tracing::error!("No player found");
                    return Task::none();
                };
                let player = player_status.player.clone();

                match r {
                    MprisRequest::Play => tokio::spawn(async move {
                        let res = player.play().await;
                        if let Err(err) = res {
                            tracing::error!("Error playing: {}", err);
                        }
                    }),
                    MprisRequest::Pause => tokio::spawn(async move {
                        let res = player.pause().await;
                        if let Err(err) = res {
                            tracing::error!("Error pausing: {}", err);
                        }
                    }),
                    MprisRequest::Next => tokio::spawn(async move {
                        let res = player.next().await;
                        if let Err(err) = res {
                            tracing::error!("Error playing next: {}", err);
                        }
                    }),
                    MprisRequest::Previous => tokio::spawn(async move {
                        let res = player.previous().await;
                        if let Err(err) = res {
                            tracing::error!("Error playing previous: {}", err);
                        }
                    }),
                    MprisRequest::Raise => tokio::spawn(async move {
                        let res = player.media_player().await;
                        if let Err(err) = res {
                            tracing::error!("Error fetching MediaPlayer: {}", err);
                        } else {
                            let res = res.unwrap().raise().await;
                            if let Err(err) = res {
                                tracing::error!("Error raising client: {}", err);
                            }
                        }
                    }),
                };
            }
            Message::OpenSettings => {
                let exec = "cosmic-settings sound".to_string();
                if let Some(tx) = self.token_tx.as_ref() {
                    let _ = tx.send(TokenRequest {
                        app_id: Self::APP_ID.to_string(),
                        exec,
                    });
                } else {
                    tracing::error!("Wayland tx is None");
                }
            }
            Message::Token(u) => match u {
                TokenUpdate::Init(tx) => {
                    self.token_tx = Some(tx);
                }
                TokenUpdate::Finished => {
                    self.token_tx = None;
                }
                TokenUpdate::ActivationToken { token, .. } => {
                    let mut cmd = std::process::Command::new("cosmic-settings");
                    cmd.arg("sound");
                    if let Some(token) = token {
                        cmd.env("XDG_ACTIVATION_TOKEN", &token);
                        cmd.env("DESKTOP_STARTUP_ID", &token);
                    }
                    tokio::spawn(cosmic::process::spawn(cmd));
                }
            },
            Message::Surface(a) => {
                return cosmic::task::message(cosmic::Action::Cosmic(
                    cosmic::app::Action::Surface(a),
                ));
            }
            Message::Client(client) => {
                if let Some(client) = Arc::into_inner(client) {
                    self.audio_client = Some(Rc::new(RefCell::new(client)));
                    self.model = model::Model::default();
                }
            }
        }

        Task::none()
    }

    fn subscription(&self) -> Subscription<Message> {
        Subscription::batch([
            self.core.watch_config(Self::APP_ID).map(|u| {
                for err in u.errors {
                    tracing::error!(?err, "Error watching config");
                }
                Message::ConfigChanged(u.config)
            }),
            mpris_subscription::mpris_subscription(0).map(Message::Mpris),
            activation_token_subscription(0).map(Message::Token),
            Subscription::run(|| {
                iced::stream::channel(
                    1,
                    move |mut emitter: futures::channel::mpsc::Sender<_>| async move {
                        loop {
                            let mut client = match audio_client::connect().await {
                                Ok(client) => client,
                                Err(why) => {
                                    if let zlink::Error::Io(ref why) = why
                                        && why.kind() == std::io::ErrorKind::NotFound
                                    {
                                        tracing::error!(
                                            "cosmic-settings-daemon varlink service not found."
                                        );
                                    } else {
                                        tracing::error!(
                                            ?why,
                                            "failed to connect to cosmic-settings's varlink service"
                                        );
                                    }

                                    tokio::time::sleep(std::time::Duration::from_secs(3)).await;
                                    continue;
                                }
                            };

                            if let Ok(Ok(mut stream)) = client.recv_events().await {
                                _ = emitter.send(Message::Client(Arc::new(client))).await;
                                while let Some(message) = stream.next().await {
                                    match message {
                                        Ok(event) => {
                                            _ = emitter.send(Message::Subscription(event)).await;
                                        }
                                        Err(why) => {
                                            tracing::error!(?why, "event error");
                                        }
                                    }
                                }
                            }
                        }
                    },
                )
            }),
            rectangle_tracker_subscription(0).map(|update| Message::Rectangle(update.1)),
        ])
    }

    fn view(&self) -> Element<'_, Message> {
        let btn = self
            .core
            .applet
            .icon_button(self.output_icon_name())
            .on_press_down(Message::TogglePopup);

        const WHEEL_STEP: f32 = 5.0; // 5% per wheel event
        let btn = crate::mouse_area::MouseArea::new(btn).on_mouse_wheel(|delta| {
            let scroll_vector = match delta {
                iced::mouse::ScrollDelta::Lines { y, .. } => y.signum() * WHEEL_STEP, // -1/0/1
                iced::mouse::ScrollDelta::Pixels { y, .. } => y.signum(),             // -1/0/1
            };
            if scroll_vector == 0.0 {
                return Message::Ignore;
            }

            let new_volume = (self.model.active_sink.volume as f64 + (scroll_vector as f64))
                .clamp(0.0, self.max_sink_volume as f64);
            Message::SetSinkVolume(new_volume as u32)
        });

        let mut has_playback_buttons = false;
        let playback_buttons = (!self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {
            // if we have a configure for width and height, we're in a overflow popup
            c.width > 0. && c.height > 0.
        }))
        .then(|| self.playback_buttons());

        let mut ret = if let Some(playback_buttons) = playback_buttons
            && !playback_buttons.is_empty()
        {
            has_playback_buttons = true;
            Element::from(match self.core.applet.anchor {
                PanelAnchor::Left | PanelAnchor::Right => Element::from(
                    applet_column::Column::with_children(playback_buttons)
                        .push(btn)
                        .align_x(Alignment::Center)
                        .height(Length::Shrink)
                        // TODO configurable variable from the panel?
                        .spacing(
                            -(self.core.applet.suggested_padding(true).0 as f32)
                                * self.core.applet.padding_overlap,
                        ),
                ),
                PanelAnchor::Top | PanelAnchor::Bottom => {
                    applet_row::Row::with_children(playback_buttons)
                        .push(btn)
                        .align_y(Alignment::Center)
                        .width(Length::Shrink)
                        // TODO configurable variable from the panel?
                        .spacing(
                            -(self.core.applet.suggested_padding(true).0 as f32)
                                * self.core.applet.padding_overlap,
                        )
                        .into()
                }
            })
        } else {
            btn.into()
        };

        if let Some(tracker) = self.rectangle_tracker.as_ref()
            && has_playback_buttons
        {
            ret = tracker.container(0, ret).into()
        }

        if !self.core.applet.suggested_bounds.as_ref().is_some_and(|c| {
            // if we have a configure for width and height, we're in a overflow popup
            c.width > 0. && c.height > 0.
        }) {
            ret = self.core.applet.autosize_window(ret).into();
        }
        ret
    }

    fn view_window(&self, _id: window::Id) -> Element<'_, Message> {
        let Spacing {
            space_xxs, space_s, ..
        } = theme::active().cosmic().spacing;

        let sink: Option<&str> = self
            .model
            .sinks
            .active()
            .map(|pos| self.model.sinks.sorted_display[pos].as_ref());
        let source: Option<&str> = self
            .model
            .sources
            .active()
            .map(|pos| self.model.sources.sorted_display[pos].as_ref());

        let mut audio_content = {
            let output_slider = slider(
                0..=self.max_sink_volume,
                self.model.active_sink.volume,
                Message::SetSinkVolume,
            )
            .width(Length::FillPortion(5))
            .breakpoints(self.sink_breakpoints);

            let input_slider = slider(
                0..=self.max_source_volume,
                self.model.active_source.volume,
                Message::SetSourceVolume,
            )
            .width(Length::FillPortion(5))
            .breakpoints(self.source_breakpoints);

            column![
                padded_control(
                    row![
                        button::icon(
                            icon::from_name(self.output_icon_name())
                                .size(24)
                                .symbolic(true),
                        )
                        .class(cosmic::theme::Button::Icon)
                        .icon_size(24)
                        .line_height(24)
                        .on_press(Message::ToggleSinkMute),
                        output_slider,
                        container(text(&self.model.active_sink.volume_text).size(16))
                            .width(Length::FillPortion(1))
                            .align_x(Alignment::End)
                    ]
                    .spacing(12)
                    .align_y(Alignment::Center)
                ),
                padded_control(
                    row![
                        button::icon(
                            icon::from_name(self.input_icon_name())
                                .size(24)
                                .symbolic(true),
                        )
                        .class(cosmic::theme::Button::Icon)
                        .icon_size(24)
                        .line_height(24)
                        .on_press(Message::ToggleSourceMute),
                        input_slider,
                        container(text(&self.model.active_source.volume_text).size(16))
                            .width(Length::FillPortion(1))
                            .align_x(Alignment::End)
                    ]
                    .spacing(12)
                    .align_y(Alignment::Center)
                ),
                padded_control(divider::horizontal::default()).padding([space_xxs, space_s]),
                revealer(
                    self.is_open == IsOpen::Output,
                    fl!("output"),
                    match sink {
                        Some(sink) => sink.to_owned(),
                        None => fl!("no-device"),
                    },
                    &self.model.sinks.sorted_display,
                    Message::OutputToggle,
                    Message::SetDefaultSink,
                ),
                revealer(
                    self.is_open == IsOpen::Input,
                    fl!("input"),
                    match source {
                        Some(source) => source.to_owned(),
                        None => fl!("no-device"),
                    },
                    &self.model.sources.sorted_display,
                    Message::InputToggle,
                    Message::SetDefaultSource,
                )
            ]
            .align_x(Alignment::Start)
        };

        if let Some(s) = self.player_status.as_ref() {
            let mut elements = Vec::with_capacity(5);

            if let Some(icon_path) = s.icon.clone() {
                elements.push(icon(icon::from_path(icon_path)).size(36).into());
            }

            let title = if let Some(title) = s.title.as_ref() {
                if title.chars().count() > 22 {
                    let mut title_trunc = title.chars().take(20).collect::<String>();
                    title_trunc.push_str("...");
                    title_trunc
                } else {
                    title.to_string()
                }
            } else {
                String::new()
            };

            let artists = if let Some(artists) = s.artists.as_ref() {
                let artists = artists.join(", ");
                if artists.chars().count() > 27 {
                    let mut artists_trunc = artists.chars().take(25).collect::<String>();
                    artists_trunc.push_str("...");
                    artists_trunc
                } else {
                    artists
                }
            } else {
                fl!("unknown-artist")
            };

            elements.push(
                column![
                    text::body(title).width(Length::Shrink),
                    text::caption(artists).width(Length::Shrink),
                ]
                .width(Length::FillPortion(5))
                .into(),
            );

            let mut control_elements = Vec::with_capacity(4);
            control_elements.push(space::horizontal().width(Length::Fill).into());
            if let Some(go_prev) = self.go_previous(32) {
                control_elements.push(go_prev);
            }
            if let Some(play) = self.is_play() {
                control_elements.push(
                    button::icon(icon::from_name(if play { PLAY } else { PAUSE }).symbolic(true))
                        .extra_small()
                        .class(cosmic::theme::Button::AppletIcon)
                        .on_press(if play {
                            Message::MprisRequest(MprisRequest::Play)
                        } else {
                            Message::MprisRequest(MprisRequest::Pause)
                        })
                        .into(),
                );
            }
            if let Some(go_next) = self.go_next(32) {
                control_elements.push(go_next);
            }
            let control_cnt = control_elements.len() as u16;
            elements.push(
                Row::with_children(control_elements)
                    .align_y(Alignment::Center)
                    .width(Length::FillPortion(control_cnt.saturating_add(1)))
                    .spacing(8)
                    .into(),
            );

            audio_content = audio_content
                .push(padded_control(divider::horizontal::default()).padding([space_xxs, space_s]));
            audio_content = audio_content.push(
                menu_button(
                    Row::with_children(elements)
                        .align_y(Alignment::Center)
                        .spacing(8),
                )
                .on_press(Message::MprisRequest(MprisRequest::Raise))
                .padding(menu_control_padding()),
            );
        }
        let content = column![
            audio_content,
            padded_control(divider::horizontal::default()).padding([space_xxs, space_s]),
            padded_control(
                toggler(self.config.show_media_controls_in_top_panel)
                    .on_toggle(Message::ToggleMediaControlsInTopPanel)
                    .label(fl!("show-media-controls"))
                    .text_size(14)
                    .width(Length::Fill)
            ),
            padded_control(divider::horizontal::default()).padding([space_xxs, space_s]),
            menu_button(text::body(fl!("sound-settings"))).on_press(Message::OpenSettings)
        ]
        .align_x(Alignment::Start)
        .padding([8, 0]);

        self.core.applet.popup_container(container(content)).into()
    }

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

fn revealer<'a>(
    open: bool,
    title: String,
    selected: String,
    devices: &'a [Arc<str>],
    toggle: Message,
    mut change: impl FnMut(usize) -> Message + 'static,
) -> widget::Column<'a, Message, crate::Theme, Renderer> {
    if open {
        devices.iter().enumerate().fold(
            column![revealer_head(open, title, selected, toggle)].width(Length::Fill),
            move |col, (id, name)| {
                col.push(
                    text::body(name.as_ref())
                        .apply(menu_button)
                        .on_press(change(id))
                        .width(Length::Fill)
                        .padding([8, 48]),
                )
            },
        )
    } else {
        column![revealer_head(open, title, selected, toggle)]
    }
}

fn revealer_head(
    _open: bool,
    title: String,
    selected: String,
    toggle: Message,
) -> cosmic::widget::Button<'static, Message> {
    cosmic::widget::column::with_capacity(2)
        .push(text::body(title).width(Length::Fill))
        .push(text::caption(selected))
        .apply(menu_button)
        .on_press(toggle)
}
