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

pub mod shortcuts;

use std::cmp;

use cosmic::app::{ContextDrawer, context_drawer};
use cosmic::cosmic_config::{self, ConfigSet};
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, ListColumn, button, container, icon, list, row, settings};
use cosmic::{Apply, Element, Task, theme};
use cosmic_comp_config::{KeyboardConfig, NumlockState, XkbConfig};
use cosmic_settings_page::{self as page, Section, section};
use itertools::Itertools;
use slotmap::{DefaultKey, Key, SlotMap};

/// Contains all options for mapping a [SpecialKey].
/// The available options differ for each key being mapped.
enum SpecialKeyAlternative {
    AltLeft,
    AltRight,
    Ctrl,
    CtrlRight,
    SwapWithCtrl,
    Super,
    SuperLeft,
    SuperRight,
    CapsLock,
    ScrollLock,
    MenuKey,
    Backspace,
    Escape,
    SwapWithEscape,
    PrintScreen,
    None,
}
impl SpecialKeyAlternative {
    fn display_text(&self) -> String {
        match self {
            Self::AltLeft => fl!("keyboard-special-char", "alt-left"),
            Self::AltRight => fl!("keyboard-special-char", "alt-right"),
            Self::Ctrl => fl!("keyboard-special-char", "ctrl"),
            Self::CtrlRight => fl!("keyboard-special-char", "ctrl-right"),
            Self::SwapWithCtrl => fl!("keyboard-special-char", "swap-with-ctrl"),
            Self::Super => fl!("keyboard-special-char", "super"),
            Self::SuperLeft => fl!("keyboard-special-char", "super-left"),
            Self::SuperRight => fl!("keyboard-special-char", "super-right"),
            Self::CapsLock => fl!("keyboard-special-char", "caps"),
            Self::ScrollLock => fl!("keyboard-special-char", "scroll-lock"),
            Self::MenuKey => fl!("keyboard-special-char", "menu"),
            Self::Backspace => fl!("keyboard-special-char", "backspace"),
            Self::Escape => fl!("keyboard-special-char", "escape"),
            Self::SwapWithEscape => fl!("keyboard-special-char", "swap-with-escape"),
            Self::PrintScreen => fl!("keyboard-special-char", "print-screen"),
            Self::None => fl!("keyboard-special-char", "none"),
        }
    }
}

/// The second/right value is the key identifier, which is how it will be saved to the configuration.
/// These values are different for each [SpecialKey], even for the same [SpecialKeyAlternative].
/// The identifier starts with a prefix defined by [SpecialKey::prefixes].
type SpecialKeyOption = (SpecialKeyAlternative, &'static str);

/// Available options for [SpecialKey::Compose].
static COMPOSE_OPTIONS: &[SpecialKeyOption] = &[
    // ("Left Alt", "compose:lalt"), XXX?
    (SpecialKeyAlternative::AltRight, "compose:ralt"),
    (SpecialKeyAlternative::SuperLeft, "compose:lwin"),
    (SpecialKeyAlternative::SuperRight, "compose:rwin"),
    (SpecialKeyAlternative::MenuKey, "compose:menu"),
    (SpecialKeyAlternative::CtrlRight, "compose:rctrl"),
    (SpecialKeyAlternative::CapsLock, "compose:caps"),
    (SpecialKeyAlternative::ScrollLock, "compose:sclk"),
    (SpecialKeyAlternative::PrintScreen, "compose:prsc"),
];
/// Available options for [SpecialKey::AlternateCharacters].
static ALTERNATE_CHARACTER_OPTIONS: &[SpecialKeyOption] = &[
    (SpecialKeyAlternative::AltLeft, "lv3:lalt_switch"),
    (SpecialKeyAlternative::AltRight, "lv3:ralt_switch"),
    (SpecialKeyAlternative::SuperLeft, "lv3:lwin_switch"),
    (SpecialKeyAlternative::SuperRight, "lv3:rwin_switch"),
    (SpecialKeyAlternative::MenuKey, "lv3:menu_switch"),
    // ("Right Ctrl", "lv3:"), XXX
    (SpecialKeyAlternative::CapsLock, "lv3:caps_switch"),
    // ("Scroll Lock", "lv3:"), XXX
    // ("Print Screen", "lv3"), XXX
];
/// Available options for [SpecialKey::CapsLock].
static CAPS_LOCK_OPTIONS: &[SpecialKeyOption] = &[
    (SpecialKeyAlternative::Escape, "caps:escape"),
    (SpecialKeyAlternative::SwapWithEscape, "caps:swapescape"),
    (SpecialKeyAlternative::Backspace, "caps:backspace"),
    (SpecialKeyAlternative::Super, "caps:super"),
    (SpecialKeyAlternative::Ctrl, "caps:ctrl_modifier"),
    (SpecialKeyAlternative::SwapWithCtrl, "ctrl:swapcaps"),
];

#[derive(Clone, Debug)]
pub enum Message {
    ExpandInputSourcePopover(Option<DefaultKey>),
    InputSourceSearch(String),
    OpenSpecialCharacterContext(SpecialKey),
    OpenNumlockContext,
    ShowInputSourcesContext,
    SourceAdd(DefaultKey),
    SourceContext(SourceContext),
    SpecialCharacterSelect(Option<&'static str>),
    SetRepeatKeysDelay(u32),
    SetRepeatKeysRate(u32),
    SetShowExtendedInputSources(bool),
    SetNumlockState(NumlockState),
}

#[derive(Clone, Debug)]
pub enum SourceContext {
    MoveDown(DefaultKey),
    MoveUp(DefaultKey),
    Remove(DefaultKey),
    Settings(DefaultKey),
    ViewLayout(DefaultKey),
}

pub type Locale = String;
pub type Variant = String;
pub type Description = String;

#[derive(Clone, Debug, PartialEq, Eq)]
pub enum LayoutSource {
    Base,
    Extra,
    Custom,
}

const KB_REPEAT_DELAY_DEFAULT: u32 = 600;
const KB_REPEAT_RATE_DEFAULT: u32 = 25;
const KB_REPEAT_DELAY_MAX: u32 = 1000;
const KB_REPEAT_DELAY_MIN: u32 = 200;
const KB_REPEAT_RATE_MAX: u32 = 45;
const KB_REPEAT_RATE_MIN: u32 = 5;

const COSMIC_COMP_CONFIG: &str = "com.system76.CosmicComp";
const COSMIC_COMP_CONFIG_VERSION: u64 = 1;

pub struct Page {
    entity: page::Entity,
    config: cosmic_config::Config,
    context: Option<Context>,
    input_source_search: String,
    xkb: XkbConfig,
    keyboard_config: KeyboardConfig,
    keyboard_layouts: SlotMap<DefaultKey, (Locale, Variant, Description, LayoutSource)>,
    active_layouts: Vec<DefaultKey>,
    expanded_source_popover: Option<DefaultKey>,
    show_extended_input_sources: bool,
}

impl Default for Page {
    fn default() -> Self {
        let config =
            cosmic_config::Config::new(COSMIC_COMP_CONFIG, COSMIC_COMP_CONFIG_VERSION).unwrap();

        Self {
            entity: page::Entity::null(),
            context: None,
            expanded_source_popover: None,
            keyboard_layouts: SlotMap::new(),
            active_layouts: Vec::new(),
            xkb: XkbConfig::default(),
            keyboard_config: KeyboardConfig::default(),
            input_source_search: String::new(),
            show_extended_input_sources: false,
            config,
        }
    }
}

enum Context {
    ShowInputSourcesContext,
    SpecialCharacter(SpecialKey),
    NumlockState,
}

#[derive(Copy, Clone, Debug)]
pub enum SpecialKey {
    AlternateCharacters,
    Compose,
    CapsLock,
}

impl SpecialKey {
    pub fn title(self) -> String {
        match self {
            Self::Compose => fl!("keyboard-special-char", "compose"),
            Self::AlternateCharacters => fl!("keyboard-special-char", "alternate"),
            Self::CapsLock => fl!("keyboard-special-char", "caps"),
        }
    }

    pub fn prefixes(self) -> &'static [&'static str] {
        match self {
            Self::Compose => &["compose:"],
            Self::AlternateCharacters => &["lv3:"],
            Self::CapsLock => &["caps:", "ctrl:"],
        }
    }
}

fn popover_menu_row(
    id: DefaultKey,
    label: String,
    message: impl Fn(DefaultKey) -> SourceContext + 'static,
) -> cosmic::Element<'static, Message> {
    let spacing = theme::spacing();
    widget::text::body(label)
        .align_y(Alignment::Center)
        .apply(button::custom)
        .padding([spacing.space_xxxs, spacing.space_xs])
        .width(Length::Fill)
        .class(theme::Button::MenuItem)
        .on_press(Message::SourceContext(message(id)))
        .apply(Element::from)
}

fn popover_menu(id: DefaultKey) -> cosmic::Element<'static, Message> {
    widget::column::with_children([
        popover_menu_row(
            id,
            fl!("keyboard-sources", "move-up"),
            SourceContext::MoveUp,
        ),
        popover_menu_row(
            id,
            fl!("keyboard-sources", "move-down"),
            SourceContext::MoveDown,
        ),
        widget::divider::horizontal::default()
            .apply(widget::container)
            .padding(8)
            .into(),
        popover_menu_row(
            id,
            fl!("keyboard-sources", "settings"),
            SourceContext::Settings,
        ),
        popover_menu_row(
            id,
            fl!("keyboard-sources", "view-layout"),
            SourceContext::ViewLayout,
        ),
        popover_menu_row(id, fl!("keyboard-sources", "remove"), SourceContext::Remove),
    ])
    .width(Length::Fixed(200.0))
    .apply(widget::container)
    .padding(theme::spacing().space_xxs)
    .class(theme::Container::Dropdown)
    .into()
}

fn popover_button(id: DefaultKey, expanded: bool) -> cosmic::Element<'static, Message> {
    let on_press = Message::ExpandInputSourcePopover(if expanded { None } else { Some(id) });

    let button = button::icon(icon::from_name("view-more-symbolic"))
        .extra_small()
        .on_press(on_press);

    if expanded {
        widget::popover(button)
            .position(widget::popover::Position::Bottom)
            .popup(popover_menu(id))
            .on_close(Message::ExpandInputSourcePopover(None))
            .into()
    } else {
        button.into()
    }
}

fn input_source(
    id: DefaultKey,
    description: &str,
    expanded_source_popover: Option<DefaultKey>,
) -> cosmic::Element<'_, Message> {
    let expanded = expanded_source_popover.is_some_and(|expanded_id| expanded_id == id);

    settings::item(description, popover_button(id, expanded)).into()
}

fn special_char_radio_row<'a>(
    desc: String,
    value: Option<&'static str>,
    current_value: Option<&'a str>,
) -> list::ListButton<'a, Message> {
    settings::item::builder(desc).radio(value, Some(current_value), |_| {
        Message::SpecialCharacterSelect(value)
    })
}

impl page::Page<crate::pages::Message> for Page {
    fn set_id(&mut self, entity: page::Entity) {
        self.entity = entity;
    }

    fn content(
        &self,
        sections: &mut SlotMap<section::Entity, Section<crate::pages::Message>>,
    ) -> Option<page::Content> {
        Some(vec![
            sections.insert(input_sources()),
            sections.insert(special_character_entry()),
            sections.insert(keyboard_shortcuts()),
            sections.insert(keyboard_typing_assist()),
            sections.insert(keyboard_num_lock()),
        ])
    }

    fn info(&self) -> page::Info {
        page::Info::new("keyboard", "input-keyboard-symbolic")
            .title(fl!("keyboard"))
            .description(fl!("xdg-entry-keyboard-comment"))
    }

    fn context_drawer(&self) -> Option<ContextDrawer<'_, crate::pages::Message>> {
        self.context.as_ref().map(|context| match context {
            Context::ShowInputSourcesContext => {
                let search = widget::search_input("", &self.input_source_search)
                    .on_input(Message::InputSourceSearch)
                    .on_clear(Message::InputSourceSearch(String::new()))
                    .apply(Element::from)
                    .map(crate::pages::Message::Keyboard);

                context_drawer(
                    self.add_input_source_view(),
                    crate::pages::Message::CloseContextDrawer,
                )
                .title(fl!("keyboard-sources", "add"))
                .header(search)
            }
            Context::SpecialCharacter(special_key) => context_drawer(
                self.special_character_key_view(*special_key)
                    .map(crate::pages::Message::Keyboard),
                crate::pages::Message::CloseContextDrawer,
            )
            .title(special_key.title()),
            Context::NumlockState => context_drawer(
                self.numlock_state_view()
                    .map(crate::pages::Message::Keyboard),
                crate::pages::Message::CloseContextDrawer,
            )
            .title(fl!("keyboard-numlock-boot", "set")),
        })
    }

    fn on_enter(&mut self) -> Task<crate::pages::Message> {
        self.xkb = super::get_config(&self.config, "xkb_config");
        self.keyboard_config = super::get_config(&self.config, "keyboard_config");
        match (
            xkb_data::keyboard_layouts(),
            xkb_data::extra_keyboard_layouts(),
        ) {
            (Ok(base_layouts), Ok(extra_layouts)) => {
                self.active_layouts.clear();
                self.keyboard_layouts.clear();

                let mut sorted_layouts = base_layouts
                    .layouts()
                    .iter()
                    .map(|layout| (layout, LayoutSource::Base))
                    .chain(
                        extra_layouts
                            .layouts()
                            .iter()
                            .map(|layout| (layout, LayoutSource::Extra)),
                    )
                    .collect::<Vec<_>>();

                // Add user-defined layouts if any are found
                let user_layouts = xkb_data::user_keyboard_layouts();
                match user_layouts {
                    Ok(ref custom) => {
                        sorted_layouts.extend(
                            custom
                                .layouts()
                                .iter()
                                .map(|layout| (layout, LayoutSource::Custom)),
                        );
                    }
                    Err(why) => {
                        tracing::error!(?why, "failed to get user keyboard layouts");
                    }
                }

                sorted_layouts.sort_unstable_by(|(a, _), (b, _)| {
                    match (a.name(), b.name()) {
                        // Place US at the top of the list as it's the default
                        ("us", _) => cmp::Ordering::Less,
                        (_, "us") => cmp::Ordering::Greater,
                        // Place custom at the bottom
                        ("custom", _) => cmp::Ordering::Greater,
                        (_, "custom") => cmp::Ordering::Less,
                        // Compare everything else by description because it looks nicer (e.g. all
                        // English grouped together)
                        _ => a.description().cmp(b.description()),
                    }
                });

                for (layout, source) in sorted_layouts {
                    self.keyboard_layouts.insert((
                        layout.name().to_owned(),
                        String::new(),
                        gettextrs::dgettext("xkeyboard-config", layout.description()),
                        source.clone(),
                    ));

                    if let Some(variants) = layout.variants().map(|variants| {
                        variants.iter().map(|variant| {
                            (
                                layout.name().to_owned(),
                                variant.name().to_owned(),
                                gettextrs::dgettext("xkeyboard-config", variant.description()),
                                source.clone(),
                            )
                        })
                    }) {
                        let mut variants: Vec<_> = variants.collect();
                        variants.sort_unstable_by(|(_, _, desc_a, _), (_, _, desc_b, _)| {
                            desc_a.cmp(desc_b)
                        });

                        for (layout_name, name, description, source) in variants {
                            self.keyboard_layouts
                                .insert((layout_name, name, description, source));
                        }
                    }
                }

                // Xkb layouts currently enabled.
                let layouts = if self.xkb.layout.is_empty() {
                    "us"
                } else {
                    &self.xkb.layout
                }
                .split_terminator(',');

                // Xkb variants for each layout. Repeat empty strings in case there's more layouts than variants.
                let variants = self
                    .xkb
                    .variant
                    .split_terminator(',')
                    .chain(std::iter::repeat(""));

                for (layout, variant) in layouts.zip(variants) {
                    for (id, (xkb_layout, xkb_variant, _desc, _source)) in &self.keyboard_layouts {
                        if layout == xkb_layout && variant == xkb_variant {
                            self.active_layouts.push(id);
                            break;
                        }
                    }
                }
            }
            (Err(why), _) | (_, Err(why)) => {
                tracing::error!(?why, "failed to get keyboard layouts");
            }
        }

        Task::none()
    }

    fn on_leave(&mut self) -> Task<crate::pages::Message> {
        self.active_layouts = Vec::new();
        self.keyboard_layouts = SlotMap::new();
        self.input_source_search = String::new();
        Task::none()
    }
}

impl Page {
    pub fn update(&mut self, message: Message) -> Task<crate::app::Message> {
        match message {
            Message::InputSourceSearch(search) => {
                self.input_source_search = search;
            }

            Message::SourceAdd(id) => {
                self.context = None;

                if !self.active_layouts.contains(&id) {
                    self.active_layouts.push(id);
                    self.update_xkb_config();
                }
            }

            Message::SourceContext(context_message) => {
                self.expanded_source_popover = None;

                match context_message {
                    SourceContext::MoveDown(id) => {
                        if let Some(pos) =
                            self.active_layouts.iter().position(|&active| active == id)
                            && pos + 1 < self.active_layouts.len()
                        {
                            self.active_layouts.swap(pos, pos + 1);
                            self.update_xkb_config();
                        }
                    }

                    SourceContext::MoveUp(id) => {
                        if let Some(pos) =
                            self.active_layouts.iter().position(|&active| active == id)
                            && pos > 0
                        {
                            self.active_layouts.swap(pos, pos - 1);
                            self.update_xkb_config();
                        }
                    }

                    SourceContext::Remove(id) => {
                        if let Some(pos) =
                            self.active_layouts.iter().position(|&active| active == id)
                        {
                            let _removed = self.active_layouts.remove(pos);
                            self.update_xkb_config();
                        }
                    }

                    SourceContext::Settings(_id) => {
                        eprintln!("settings not implemented");
                    }

                    SourceContext::ViewLayout(_id) => {
                        eprintln!("view layout not implemented");
                    }
                }
            }

            Message::ShowInputSourcesContext => {
                self.context = Some(Context::ShowInputSourcesContext);
                return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
            }

            Message::ExpandInputSourcePopover(value) => {
                self.expanded_source_popover = value;
            }

            Message::OpenSpecialCharacterContext(key) => {
                self.context = Some(Context::SpecialCharacter(key));
                return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
            }

            Message::OpenNumlockContext => {
                self.context = Some(Context::NumlockState);
                return cosmic::task::message(crate::app::Message::OpenContextDrawer(self.entity));
            }

            Message::SpecialCharacterSelect(id) => {
                if let Some(Context::SpecialCharacter(special_key)) = self.context {
                    let options = self.xkb.options.as_deref().unwrap_or_default();
                    let prefixes = special_key.prefixes();
                    let new_options = options
                        .split(',')
                        .filter(|x| !prefixes.iter().any(|prefix| x.starts_with(prefix)))
                        .chain(id)
                        .join(",");

                    self.xkb.options = Some(new_options).filter(|x| !x.is_empty());

                    if let Err(err) = self.config.set("xkb_config", &self.xkb) {
                        tracing::error!(?err, "Failed to set config 'xkb_config'");
                    }
                }
            }
            Message::SetRepeatKeysDelay(delay) => {
                self.xkb.repeat_delay = delay;
                self.update_xkb_config();
            }
            Message::SetRepeatKeysRate(rate) => {
                self.xkb.repeat_rate = rate;
                self.update_xkb_config();
            }
            Message::SetShowExtendedInputSources(value) => {
                self.show_extended_input_sources = value;
            }
            Message::SetNumlockState(numlock_state) => {
                self.keyboard_config.numlock_state = numlock_state;
                if let Err(err) = self.config.set("keyboard_config", &self.keyboard_config) {
                    tracing::error!(?err, "Failed to set config 'keyboard_config'");
                }
            }
        }

        Task::none()
    }

    pub fn add_input_source_view(&self) -> Element<'_, crate::pages::Message> {
        let space_l = theme::spacing().space_l;

        let toggler = settings::section().add(
            settings::item::builder(fl!("show-extended-input-sources")).toggler(
                self.show_extended_input_sources,
                Message::SetShowExtendedInputSources,
            ),
        );

        let mut list = widget::list_column();

        let search_input = &self.input_source_search.trim().to_lowercase();

        for (id, (_locale, variant, description, source)) in &self.keyboard_layouts {
            if (search_input.is_empty() || description.to_lowercase().contains(search_input))
                && (source != &LayoutSource::Extra || self.show_extended_input_sources)
            {
                list = list.add(self.input_source_item(id, description, !variant.is_empty()));
            }
        }

        widget::column::with_capacity(2)
            .spacing(space_l)
            .push(toggler)
            .push(list)
            .apply(Element::from)
            .map(crate::pages::Message::Keyboard)
    }

    fn input_source_item<'a>(
        &self,
        id: DefaultKey,
        description: &'a str,
        indent: bool,
    ) -> Element<'a, Message> {
        let is_added = self.active_layouts.contains(&id);
        let button_text = if is_added { fl!("added") } else { fl!("add") };

        let add_button = widget::button::text(button_text).on_press_maybe(if is_added {
            None
        } else {
            Some(Message::SourceAdd(id))
        });

        let button = widget::settings::item::builder(description).control(add_button);

        if indent {
            container(button).padding([0, 0, 0, 16]).into()
        } else {
            button.into()
        }
    }

    fn special_character_key_view(&self, special_key: SpecialKey) -> cosmic::Element<'_, Message> {
        let (options, description) = match special_key {
            SpecialKey::Compose => (
                COMPOSE_OPTIONS,
                Some(fl!("keyboard-special-char", "compose-desc")),
            ),
            SpecialKey::AlternateCharacters => (ALTERNATE_CHARACTER_OPTIONS, None),
            SpecialKey::CapsLock => (CAPS_LOCK_OPTIONS, None),
        };
        let prefixes = special_key.prefixes();
        let current = self
            .xkb
            .options
            .iter()
            .flat_map(|x| x.split(','))
            .find(|x| prefixes.iter().any(|prefix| x.starts_with(prefix)));

        // TODO layout default

        let mut list = cosmic::widget::list_column();

        if matches!(special_key, SpecialKey::CapsLock) {
            list = list.add(special_char_radio_row(
                SpecialKeyAlternative::CapsLock.display_text(),
                None,
                current,
            ));
        } else {
            list = list.add(special_char_radio_row(
                SpecialKeyAlternative::None.display_text(),
                None,
                current,
            ));
        }

        list = options
            .iter()
            .map(|(key, id)| special_char_radio_row(key.display_text(), Some(id), current))
            .fold(list, ListColumn::add);

        widget::column::with_capacity(2)
            .spacing(theme::spacing().space_m)
            .push_maybe(description.map(widget::text::body))
            .push(list)
            .into()
    }

    fn numlock_state_view(&self) -> cosmic::Element<'_, Message> {
        let current = self.keyboard_config.numlock_state;
        let options = [
            (fl!("keyboard-numlock-boot", "off"), NumlockState::BootOff),
            (fl!("keyboard-numlock-boot", "on"), NumlockState::BootOn),
            (
                fl!("keyboard-numlock-boot", "last-boot"),
                NumlockState::LastBoot,
            ),
        ];

        let mut list = cosmic::widget::list_column();
        for (desc, state) in options {
            list = list.add(settings::item::builder(desc).radio(
                Some(state),
                Some(Some(current)),
                |_| Message::SetNumlockState(state),
            ));
        }

        list.into()
    }

    fn update_xkb_config(&mut self) {
        let result = update_xkb_config(
            &self.config,
            &mut self.xkb,
            &mut self
                .active_layouts
                .iter()
                .filter_map(|id| self.keyboard_layouts.get(*id))
                .map(|(locale, variant, _description, _source)| {
                    (locale.as_str(), variant.as_str())
                }),
        );

        if let Err(why) = result {
            tracing::error!(?why, "Failed to set config 'xkb_config'");
        }
    }
}

impl page::AutoBind<crate::pages::Message> for Page {
    fn sub_pages(page: page::Insert<crate::pages::Message>) -> page::Insert<crate::pages::Message> {
        page.sub_page::<shortcuts::Page>()
    }
}

fn input_sources() -> Section<crate::pages::Message> {
    Section::default()
        .title(fl!("keyboard-sources"))
        .view::<Page>(move |_binder, page, section| {
            // TODO Need something more custom, with drag and drop
            let mut section = settings::section().title(&section.title);

            for id in &page.active_layouts {
                if let Some((_locale, _variant, description, _source)) =
                    page.keyboard_layouts.get(*id)
                {
                    section =
                        section.add(input_source(*id, description, page.expanded_source_popover));
                }
            }

            let add_input_source = widget::button::standard(fl!("keyboard-sources", "add"))
                .on_press(Message::ShowInputSourcesContext);

            widget::column::with_capacity(2)
                .spacing(theme::spacing().space_xxs)
                .push(section)
                .push(
                    widget::container(add_input_source)
                        .width(Length::Fill)
                        .align_x(Alignment::End),
                )
                .apply(Element::from)
                .map(crate::pages::Message::Keyboard)
        })
}

fn special_character_entry() -> Section<crate::pages::Message> {
    crate::slab!(descriptions {
        alternate = fl!("keyboard-special-char", "alternate");
        compose = fl!("keyboard-special-char", "compose");
        caps = fl!("keyboard-special-char", "caps");
    });

    Section::default()
        .title(fl!("keyboard-special-char"))
        .descriptions(descriptions)
        .view::<Page>(move |_binder, _page, section| {
            let descriptions = &section.descriptions;

            settings::section()
                .title(&section.title)
                .add(crate::widget::go_next_item(
                    &descriptions[alternate],
                    Message::OpenSpecialCharacterContext(SpecialKey::AlternateCharacters),
                ))
                .add(crate::widget::go_next_item(
                    &descriptions[compose],
                    Message::OpenSpecialCharacterContext(SpecialKey::Compose),
                ))
                .add(crate::widget::go_next_item(
                    &descriptions[caps],
                    Message::OpenSpecialCharacterContext(SpecialKey::CapsLock),
                ))
                .apply(cosmic::Element::from)
                .map(crate::pages::Message::Keyboard)
        })
}

fn keyboard_shortcuts() -> Section<crate::pages::Message> {
    crate::slab!(descriptions {
        shortcuts_desc = fl!("keyboard-shortcuts", "desc");
    });

    Section::default()
        .title(fl!("keyboard-shortcuts"))
        .descriptions(descriptions)
        .view::<Page>(move |binder, _page, section| {
            let descriptions = &section.descriptions;

            let mut section = settings::section().title(&section.title);
            if let Some((shortcuts_entity, _)) = binder
                .info
                .iter()
                .find(|(_, v)| v.id == "keyboard-shortcuts")
            {
                section = section.add(crate::widget::go_next_item(
                    &descriptions[shortcuts_desc],
                    crate::pages::Message::Page(shortcuts_entity),
                ));
            }
            section.apply(cosmic::Element::from)
        })
}

fn keyboard_typing_assist() -> Section<crate::pages::Message> {
    crate::slab!(descriptions {
        repeat_delay = fl!("keyboard-typing-assist", "repeat-delay");
        repeat_rate = fl!("keyboard-typing-assist", "repeat-rate");
        short = fl!("short");
        long = fl!("long");
        slow = fl!("slow");
        fast = fl!("fast");
    });

    Section::default()
        .title(fl!("keyboard-typing-assist"))
        .descriptions(descriptions)
        .view::<Page>(move |_binder, page, section| {
            let descriptions = &section.descriptions;

            settings::section()
                .title(&section.title)
                .add(
                    settings::item::builder(&descriptions[repeat_delay])
                        .flex_control({
                            // Delay
                            let delay_slider = cosmic::widget::slider(
                                KB_REPEAT_DELAY_MIN..=KB_REPEAT_DELAY_MAX,
                                page.xkb.repeat_delay,
                                Message::SetRepeatKeysDelay,
                            )
                            .width(Length::Fill)
                            .breakpoints(&[KB_REPEAT_DELAY_DEFAULT])
                            .step(50_u32)
                            .apply(widget::container)
                            .max_width(250);

                            row::with_capacity(3)
                                .align_y(Alignment::Center)
                                .spacing(theme::spacing().space_s)
                                .push(widget::text::body(&descriptions[short]))
                                .push(delay_slider)
                                .push(widget::text::body(&descriptions[long]))
                        })
                        .align_items(Alignment::Center),
                )
                .add(
                    settings::item::builder(&descriptions[repeat_rate])
                        .flex_control({
                            // Repeat rate
                            let rate_slider = cosmic::widget::slider(
                                KB_REPEAT_RATE_MIN..=KB_REPEAT_RATE_MAX,
                                page.xkb.repeat_rate,
                                Message::SetRepeatKeysRate,
                            )
                            .width(Length::Fill)
                            .breakpoints(&[KB_REPEAT_RATE_DEFAULT])
                            .step(5_u32)
                            .apply(widget::container)
                            .max_width(250);

                            row::with_capacity(3)
                                .align_y(Alignment::Center)
                                .spacing(theme::spacing().space_s)
                                .push(widget::text::body(&descriptions[slow]))
                                .push(rate_slider)
                                .push(widget::text::body(&descriptions[fast]))
                        })
                        .align_items(Alignment::Center),
                )
                .apply(cosmic::Element::from)
                .map(crate::pages::Message::Keyboard)
        })
}

fn keyboard_num_lock() -> Section<crate::pages::Message> {
    crate::slab!(descriptions {
        boot_state = fl!("keyboard-numlock-boot", "boot-state");
    });

    Section::default()
        .title(fl!("keyboard-numlock-boot"))
        .descriptions(descriptions)
        .view::<Page>(move |_binder, _page, section| {
            let descriptions = &section.descriptions;

            settings::section()
                .title(&section.title)
                .add(crate::widget::go_next_item(
                    &descriptions[boot_state],
                    Message::OpenNumlockContext,
                ))
                .apply(cosmic::Element::from)
                .map(crate::pages::Message::Keyboard)
        })
}

fn update_xkb_config(
    config: &cosmic_config::Config,
    xkb: &mut XkbConfig,
    active_layouts: &mut dyn Iterator<Item = (&str, &str)>,
) -> Result<(), cosmic_config::Error> {
    let mut new_layout = String::new();
    let mut new_variant = String::new();

    for (locale, variant) in active_layouts {
        new_layout.push_str(locale);
        new_layout.push(',');
        new_variant.push_str(variant);
        new_variant.push(',');
    }

    let _excess_comma = new_layout.pop();
    let _excess_comma = new_variant.pop();

    xkb.layout = new_layout;
    xkb.variant = new_variant;

    config.set("xkb_config", xkb)
}
