use cosmic::iced::alignment::Horizontal;
use cosmic::iced::{Alignment, Length};
use cosmic::widget::{self, button, icon, settings, text};
use cosmic::{theme, Apply, Element, Task};
use cosmic_config::{ConfigGet, ConfigSet};
use cosmic_settings_config::shortcuts::{self, Action, Binding, Shortcuts};
use slab::Slab;
use std::borrow::Cow;
use std::io;
use std::str::FromStr;

#[derive(Clone, Debug)]
pub enum ShortcutMessage {
    AddKeybinding,
    ApplyReplace,
    CancelReplace,
    DeleteBinding(usize),
    DeleteShortcut(usize),
    EditBinding(usize, bool),
    InputBinding(usize, String),
    ResetBindings,
    ShowShortcut(usize, String),
    SubmitBinding(usize),
}

#[derive(Debug)]
pub struct ShortcutBinding {
    pub id: widget::Id,
    pub binding: Binding,
    pub input: String,
    pub editing: bool,
    pub is_default: bool,
}

#[must_use]
#[derive(Debug)]
pub struct ShortcutModel {
    pub action: Action,
    pub bindings: Slab<ShortcutBinding>,
    pub description: String,
    pub modified: u16,
}

impl ShortcutModel {
    pub fn new(defaults: &Shortcuts, shortcuts: &Shortcuts, action: Action) -> Self {
        let (bindings, modified) =
            shortcuts
                .shortcuts(&action)
                .fold((Slab::new(), 0), |(mut slab, modified), binding| {
                    let is_default = defaults.0.get(binding) == Some(&action);

                    slab.insert(ShortcutBinding {
                        id: widget::Id::unique(),
                        binding: binding.clone(),
                        input: String::new(),
                        editing: false,
                        is_default,
                    });

                    (slab, if is_default { modified } else { modified + 1 })
                });

        let mut localized_description = super::localize_action(&action);
        if let Action::Spawn(_) = &action {
            localized_description = bindings
                .iter()
                .map(|(_, shortcut)| super::localize_custom_action(&action, &shortcut.binding))
                .take(1)
                .collect();
        }

        Self {
            description: localized_description,
            modified: defaults.0.iter().filter(|(_, a)| **a == action).fold(
                modified,
                |modified, (binding, _)| {
                    if bindings.iter().any(|(_, model)| model.binding == *binding) {
                        modified
                    } else {
                        modified + 1
                    }
                },
            ),
            action,
            bindings,
        }
    }
}

#[must_use]
pub struct Model {
    pub defaults: Shortcuts,
    pub replace_dialog: Option<(usize, Binding, Action, String)>,
    pub shortcut_models: Slab<ShortcutModel>,
    pub shortcut_context: Option<usize>,
    pub config: cosmic_config::Config,
    pub custom: bool,
    pub actions: fn(&Shortcuts, &Shortcuts) -> Slab<ShortcutModel>,
}

impl Default for Model {
    fn default() -> Self {
        Self {
            defaults: Shortcuts::default(),
            replace_dialog: None,
            shortcut_models: Slab::new(),
            shortcut_context: None,
            config: shortcuts::context().unwrap(),
            custom: false,
            actions: |_, _| Slab::new(),
        }
    }
}

impl Model {
    pub fn actions(mut self, actions: fn(&Shortcuts, &Shortcuts) -> Slab<ShortcutModel>) -> Self {
        self.actions = actions;
        self
    }

    pub fn custom(mut self) -> Self {
        self.custom = true;
        self
    }

    /// Adds a new binding to the shortcuts config
    pub(super) fn config_add(&self, action: Action, binding: Binding) {
        let mut shortcuts = self.shortcuts_config();
        shortcuts.0.insert(binding, action);
        self.shortcuts_config_set(shortcuts);
    }

    /// Check if a binding is already set
    pub(super) fn config_contains(&self, binding: &Binding) -> Option<Action> {
        self.shortcuts_system_config()
            .0
            .get(binding)
            .cloned()
            .filter(|action| *action != Action::Disable)
    }

    /// Removes a binding from the shortcuts config
    pub(super) fn config_remove(&self, binding: &Binding) {
        let mut shortcuts = self.shortcuts_config();
        shortcuts.0.retain(|b, _| b != binding);
        self.shortcuts_config_set(shortcuts);
    }

    pub(super) fn context_drawer(&self) -> Option<Element<'_, ShortcutMessage>> {
        self.shortcut_context
            .as_ref()
            .map(|id| context_drawer(&self.shortcut_models, *id, self.custom))
    }

    pub(super) fn dialog(&self) -> Option<Element<'_, ShortcutMessage>> {
        if let Some(&(id, _, _, ref action)) = self.replace_dialog.as_ref() {
            if let Some(short_id) = self.shortcut_context {
                if let Some(model) = self.shortcut_models.get(short_id) {
                    if let Some(shortcut) = model.bindings.get(id) {
                        let primary_action = button::suggested(fl!("replace"))
                            .on_press(ShortcutMessage::ApplyReplace);

                        let secondary_action = button::standard(fl!("cancel"))
                            .on_press(ShortcutMessage::CancelReplace);

                        let dialog = widget::dialog(fl!("replace-shortcut-dialog"))
                            .icon(icon::from_name("dialog-warning").size(64))
                            .body(fl!(
                                "replace-shortcut-dialog",
                                "desc",
                                shortcut = shortcut.input.clone(),
                                name = shortcut
                                    .binding
                                    .description
                                    .as_ref()
                                    .unwrap_or(action)
                                    .to_owned()
                            ))
                            .primary_action(primary_action)
                            .secondary_action(secondary_action);

                        return Some(dialog.into());
                    }
                }
            }
        }

        None
    }

    pub(super) fn on_enter(&mut self) {
        let mut shortcuts = self.config.get::<Shortcuts>("defaults").unwrap_or_default();
        self.defaults = shortcuts.clone();

        if let Ok(custom) = self.config.get::<Shortcuts>("custom") {
            for (binding, action) in custom.0 {
                shortcuts.0.remove(&binding);
                shortcuts.0.insert(binding, action);
            }
        }

        self.shortcut_models = (self.actions)(&self.defaults, &shortcuts);
    }

    pub(super) fn on_clear(&mut self) {
        self.shortcut_models.clear();
    }

    /// Gets the custom configuration for keyboard shortcuts.
    pub(super) fn shortcuts_config(&self) -> Shortcuts {
        match self.config.get::<Shortcuts>("custom") {
            Ok(shortcuts) => shortcuts,
            Err(cosmic_config::Error::GetKey(_, why)) if why.kind() == io::ErrorKind::NotFound => {
                Shortcuts::default()
            }
            Err(why) => {
                tracing::error!(?why, "unable to get the current shortcuts config");
                Shortcuts::default()
            }
        }
    }

    /// Gets the system configuration for keyboard shortcuts.
    pub(super) fn shortcuts_system_config(&self) -> Shortcuts {
        let mut shortcuts = self.config.get::<Shortcuts>("defaults").unwrap_or_default();

        if let Ok(custom) = self.config.get::<Shortcuts>("custom") {
            shortcuts.0.extend(custom.0);
        }

        shortcuts
    }

    /// Writes a new configuration to the keyboard shortcuts config file.
    pub(super) fn shortcuts_config_set(&self, shortcuts: Shortcuts) {
        if let Err(why) = self.config.set("custom", shortcuts) {
            tracing::error!(?why, "failed to write shortcuts config");
        }
    }

    #[allow(clippy::too_many_lines)]
    pub(super) fn update(&mut self, message: ShortcutMessage) -> Task<crate::app::Message> {
        match message {
            ShortcutMessage::AddKeybinding => {
                if let Some(short_id) = self.shortcut_context {
                    if let Some(model) = self.shortcut_models.get_mut(short_id) {
                        // If an empty entry exists, focus it instead of creating a new input.
                        for (_, shortcut) in &mut model.bindings {
                            if shortcut.binding.is_set()
                                || Binding::from_str(&shortcut.input).is_ok()
                            {
                                continue;
                            }

                            shortcut.input.clear();

                            return widget::text_input::focus(shortcut.id.clone());
                        }

                        // Create a new input and focus it.
                        let id = widget::Id::unique();
                        model.bindings.insert(ShortcutBinding {
                            id: id.clone(),
                            binding: Binding::default(),
                            input: String::new(),
                            editing: true,
                            is_default: false,
                        });

                        return widget::text_input::focus(id);
                    }
                }
            }

            ShortcutMessage::ApplyReplace => {
                if let Some((id, new_binding, ..)) = self.replace_dialog.take() {
                    if let Some(short_id) = self.shortcut_context {
                        // Remove conflicting bindings that are saved on disk.
                        self.config_remove(&new_binding);

                        // Clear any binding that matches this in the current model
                        for (_, model) in &mut self.shortcut_models {
                            if let Some(id) = model
                                .bindings
                                .iter()
                                .find(|(_, shortcut)| shortcut.binding == new_binding)
                                .map(|(id, _)| id)
                            {
                                model.bindings.remove(id);
                                break;
                            }
                        }

                        // Update the current model and save the binding to disk.
                        if let Some(model) = self.shortcut_models.get_mut(short_id) {
                            if let Some(shortcut) = model.bindings.get_mut(id) {
                                let prev_binding = shortcut.binding.clone();

                                shortcut.binding = new_binding.clone();
                                shortcut.input.clear();
                                shortcut.editing = false;

                                let action = model.action.clone();
                                self.config_remove(&prev_binding);
                                self.config_add(action, new_binding);
                            }
                        }

                        self.on_enter();
                    }
                }
            }

            ShortcutMessage::CancelReplace => self.replace_dialog = None,

            ShortcutMessage::DeleteBinding(id) => {
                if let Some(short_id) = self.shortcut_context {
                    if let Some(model) = self.shortcut_models.get_mut(short_id) {
                        let shortcut = model.bindings.remove(id);
                        if shortcut.is_default {
                            self.config_add(Action::Disable, shortcut.binding.clone());
                        } else {
                            // if last keybind deleted, clear shortcut context
                            if model.bindings.is_empty() {
                                self.shortcut_context = None;
                            }
                            self.config_remove(&shortcut.binding);
                        }

                        self.on_enter();
                    }
                }
            }

            ShortcutMessage::DeleteShortcut(id) => {
                let model = self.shortcut_models.remove(id);
                for (_, shortcut) in model.bindings {
                    self.config_remove(&shortcut.binding);
                    self.on_enter();
                }
            }

            ShortcutMessage::EditBinding(id, enable) => {
                if let Some(short_id) = self.shortcut_context {
                    if let Some(model) = self.shortcut_models.get_mut(short_id) {
                        if let Some(shortcut) = model.bindings.get_mut(id) {
                            shortcut.editing = enable;
                            if enable {
                                shortcut.input = shortcut.binding.to_string();
                                return widget::text_input::select_all(shortcut.id.clone());
                            }
                        }
                    }
                }
            }

            ShortcutMessage::InputBinding(id, text) => {
                if let Some(short_id) = self.shortcut_context {
                    if let Some(model) = self.shortcut_models.get_mut(short_id) {
                        if let Some(shortcut) = model.bindings.get_mut(id) {
                            shortcut.input = text;
                        }
                    }
                }
            }

            // Removes all bindings from the active shortcut context, and reloads the shortcuts model.
            ShortcutMessage::ResetBindings => {
                if let Some(short_id) = self.shortcut_context {
                    if let Some(model) = self.shortcut_models.get(short_id) {
                        for (_, shortcut) in &model.bindings {
                            self.config_remove(&shortcut.binding);
                        }

                        if let Ok(defaults) = self.config.get::<Shortcuts>("defaults") {
                            for (binding, action) in defaults.0 {
                                if action == model.action {
                                    self.config_remove(&binding);
                                }
                            }
                        }
                    }

                    self.on_enter();
                }
            }

            ShortcutMessage::ShowShortcut(id, description) => {
                self.shortcut_context = Some(id);
                self.replace_dialog = None;

                let mut tasks = vec![cosmic::command::message(
                    crate::app::Message::OpenContextDrawer(description.into()),
                )];

                if let Some(model) = self.shortcut_models.get(0) {
                    if let Some(shortcut) = model.bindings.get(0) {
                        tasks.push(widget::text_input::focus(shortcut.id.clone()));
                        tasks.push(widget::text_input::select_all(shortcut.id.clone()));
                    }
                }

                return Task::batch(tasks);
            }

            ShortcutMessage::SubmitBinding(id) => {
                if let Some(short_id) = self.shortcut_context {
                    let mut apply_binding = None;

                    // Check for conflicts with the new binding.
                    if let Some(model) = self.shortcut_models.get_mut(short_id) {
                        if let Some(shortcut) = model.bindings.get_mut(id) {
                            match Binding::from_str(&shortcut.input) {
                                Ok(new_binding) => {
                                    if !new_binding.is_set() {
                                        shortcut.input.clear();
                                        return Task::none();
                                    }
                                    if let Some(action) = self.config_contains(&new_binding) {
                                        let action_str = if let Action::Spawn(_) = &action {
                                            super::localize_custom_action(&action, &new_binding)
                                        } else {
                                            super::localize_action(&action)
                                        };
                                        self.replace_dialog =
                                            Some((id, new_binding, action, action_str));
                                        return Task::none();
                                    }

                                    apply_binding = Some(new_binding);
                                }

                                Err(why) => {
                                    tracing::error!(why, "keybinding input invalid");
                                }
                            }
                        }
                    }

                    // Apply if no conflict was found.
                    if let Some(new_binding) = apply_binding {
                        if let Some(model) = self.shortcut_models.get_mut(short_id) {
                            if let Some(shortcut) = model.bindings.get_mut(id) {
                                let prev_binding = shortcut.binding.clone();

                                shortcut.binding = new_binding.clone();
                                shortcut.input.clear();
                                shortcut.editing = false;

                                let action = model.action.clone();
                                self.config_remove(&prev_binding);
                                self.config_add(action, new_binding);
                                self.on_enter();
                            }
                        }
                    }
                }
            }
        }

        Task::none()
    }

    pub(super) fn view(&self) -> Element<ShortcutMessage> {
        self.shortcut_models
            .iter()
            .map(|(id, shortcut)| shortcut_item(self.custom, id, shortcut))
            .fold(widget::list_column(), widget::ListColumn::add)
            .into()
    }
}

fn context_drawer(
    shortcuts: &Slab<ShortcutModel>,
    id: usize,
    show_action: bool,
) -> Element<ShortcutMessage> {
    let cosmic::cosmic_theme::Spacing {
        space_xxs,
        space_xs,
        space_l,
        ..
    } = theme::active().cosmic().spacing;

    let model = &shortcuts[id];

    let action = show_action.then(|| {
        let description = if let Action::Spawn(task) = &model.action {
            Cow::Borrowed(task.as_str())
        } else {
            Cow::Owned(super::localize_action(&model.action))
        };

        text::body(description)
    });

    let bindings = model.bindings.iter().enumerate().fold(
        widget::list_column().spacing(space_xxs),
        |section, (_, (bind_id, shortcut))| {
            let text: Cow<'_, str> = if !shortcut.editing && shortcut.binding.is_set() {
                Cow::Owned(shortcut.binding.to_string())
            } else {
                Cow::Borrowed(&shortcut.input)
            };

            let input = widget::editable_input("", text, shortcut.editing, move |enable| {
                ShortcutMessage::EditBinding(bind_id, enable)
            })
            .select_on_focus(true)
            .on_input(move |text| ShortcutMessage::InputBinding(bind_id, text))
            .on_submit(ShortcutMessage::SubmitBinding(bind_id))
            .padding([0, space_xs])
            .id(shortcut.id.clone())
            .into();

            let delete_button = widget::button::icon(icon::from_name("edit-delete-symbolic"))
                .on_press(ShortcutMessage::DeleteBinding(bind_id))
                .into();

            let flex_control =
                settings::flex_item_row(vec![input, delete_button]).align_items(Alignment::Center);

            section.add(flex_control)
        },
    );

    // TODO: Detect when it is necessary
    let reset_keybinding_button = if show_action {
        None
    } else {
        let button = widget::button::standard(fl!("reset-to-default"))
            .on_press(ShortcutMessage::ResetBindings);
        Some(button)
    };

    let add_keybinding_button =
        widget::button::standard(fl!("add-keybinding")).on_press(ShortcutMessage::AddKeybinding);

    let button_container = widget::row::with_capacity(2)
        .push_maybe(reset_keybinding_button)
        .push(add_keybinding_button)
        .spacing(space_xs)
        .apply(widget::container)
        .width(Length::Fill)
        .align_x(Horizontal::Right);

    widget::column::with_capacity(if show_action { 3 } else { 2 })
        .spacing(space_l)
        .push_maybe(action)
        .push(bindings)
        .push(button_container)
        .into()
}

/// Display a shortcut as a list item
fn shortcut_item(custom: bool, id: usize, data: &ShortcutModel) -> Element<ShortcutMessage> {
    #[derive(Copy, Clone, Debug)]
    enum LocalMessage {
        Remove,
        Show,
    }

    let bindings = data
        .bindings
        .iter()
        .take(3)
        .filter(|(_, shortcut)| shortcut.binding.is_set())
        .map(|(_, shortcut)| text::body(shortcut.binding.to_string()).into())
        .collect::<Vec<_>>();

    let shortcuts: Element<LocalMessage> = if bindings.is_empty() {
        text::body(fl!("disabled")).into()
    } else {
        widget::column::with_children(bindings)
            .align_x(Alignment::End)
            .into()
    };

    let modified = if data.modified == 0 {
        None
    } else {
        Some(text::body(fl!("modified", count = data.modified)))
    };

    let control = widget::row::with_capacity(4)
        .push_maybe(modified)
        .push(shortcuts)
        .push(icon::from_name("go-next-symbolic").size(16))
        .push_maybe(custom.then(|| {
            widget::button::icon(icon::from_name("edit-delete-symbolic"))
                .on_press(LocalMessage::Remove)
        }))
        .align_y(Alignment::Center)
        .spacing(8);

    settings::item::builder(&data.description)
        .flex_control(control)
        .spacing(16)
        .apply(widget::container)
        .class(theme::Container::List)
        .apply(widget::button::custom)
        .class(theme::Button::Transparent)
        .on_press(LocalMessage::Show)
        .apply(Element::from)
        .map(move |message| match message {
            LocalMessage::Show => ShortcutMessage::ShowShortcut(id, data.description.clone()),
            LocalMessage::Remove => ShortcutMessage::DeleteShortcut(id),
        })
}
